[
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 88\nexclude = .git,__pycache__\nmax-complexity = 10"
  },
  {
    "path": ".github/workflows/algolia-scraper.yml",
    "content": "name: Run Algolia Scraper\n\non:\n  push:\n    branches: [\"master\", \"develop\"]\n  pull_request:\n    branches: [\"master\", \"develop\"]\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: darrenjennings/algolia-docsearch-action@master\n        with:\n          algolia_application_id: \"1BEGBIP9SH\"\n          algolia_api_key: ${{ secrets.ALGOLIA_API_KEY }}\n          file: \"docs/algolia.config.json\"\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode/\n*.pyc\n.idea/\n__pycache__/\n*.db\n.DS_Store\nvenv/\n.venv/\ndocs/docs/.nota/config.ini\nsection-start-code.zip\nsection-end-code.zip"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"Flask-Smorest Docker\"]\n\tpath = project/using-flask-smorest-docker\n\turl = https://github.com/tecladocode/rest-api-smorest-docker"
  },
  {
    "path": ".python-version",
    "content": "3.10.0\n"
  },
  {
    "path": ".templates/lecture.md",
    "content": "---\ntitle: The lecture title goes here\ndescription: A brief description of the lecture goes here.\n---\n\n- [ ] Set metadata above\n- [ ] Start writing!\n- [ ] Create `start` folder\n- [ ] Create `end` folder\n- [ ] Write TL;DR\n- [ ] Create per-file diff between `end` and `start` (use \"Compare Folders\")\n\n\n\n# Lecture Title\n\n\n"
  },
  {
    "path": ".templates/section.md",
    "content": "---\nname: \"Section name here\"\n---\n\n# Section name here\n\nDescription of the section goes here."
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How to contribute to this course\n\n## E-book contributions\n\n### How to run the e-book\n\nClone the repo and navigate to the `docs` folder.\n\nThere, run:\n\n```\nnpm install\n```\n\nThen you can run the e-book with:\n\n```\nnpm run start\n```\n\nIf you make any changes to the e-book, please keep changes as simple as possible and create a PR with your changes into the `develop` branch.\n\nIf you are making larger changes, please create a Discussion first and let's talk about it!\n\n### Making changes to projects\n\nAll the finished projects that we cover in the course are in the `projects` folder. Making changes to these projects is done very carefully, especially after recording.\n\nPlease start a Discussion before making any changes, as doing so can make the experience for students confusing (if the videos and e-book are different).\n"
  },
  {
    "path": "README.md",
    "content": "# REST APIs with Flask and Python\n\n<p align=\"center\">\n <img src=\"assets/course-image.png\" alt=\"REST APIs with Flask and Python (Udemy banner image)\"></a>\n</p>\n\n<div align=\"center\">\n\n[![Udemy rating 4.6/5](https://img.shields.io/badge/udemy-4.6%2F5-brightgreen)](https://go.tecla.do/rest-apis-ebook) ![GitHub last commit](https://img.shields.io/github/last-commit/tecladocode/rest-apis-flask-python/develop) ![Python 3.10](https://img.shields.io/badge/python-3.10-yellow) [![Discord](https://img.shields.io/discord/614395983807250433)](https://discord.gg/78Nvd3p) [![Twitter Follow](https://img.shields.io/twitter/follow/jslvtr?style=social) ](https://twitter.com/jslvtr)\n\n</div>\n\n---\n\n<p align = \"center\">💡 A full course to teach you how to use Flask and Python to make REST APIs using multiple Flask extensions and PostgreSQL.</p>\n\n## Getting started\n\nEnrol in the course by going to [this link](https://go.tecla.do/rest-apis-ebook).\n\nThen you can come back here to download the repository. This repository contains the code that we develop in each section of the course.\n\nIf you are familiar with Git, you can use Git to download it. Otherwise, you can download it as a zip file:\n\n![Download repo as a zip file](assets/download-repo-zip.png)\n\nNext, start taking the course at the beginning! You can use the downloaded code files to support you while you go through the course.\n\nI also **strongly recommend** you code while you take the course. A good strategy is:\n\n1. Watch the video intently, optionally while taking notes.\n2. Watch again, more quickly, while typing the code together with me.\n3. Once you're done with the video, play about with the code. Make changes, break things, then fix them, and try to thoroughly understand everything the code does.\n\nIf you do this for the entire course, I guarantee you will learn how to make REST APIs using Flask and Python well and quickly. You'll still be using Google and searching for stuff every day, but so does everyone else!\n\n## Section 2: A Full Python Refresher\n\nThis section (only available on Udemy) helps programmers who are new to Python get acquainted with the language. It is not a complete-beginner Python course!\n\n## Section 3: Your first REST API\n\nThe code in this section includes a simple Flask app that accepts and returns JSON data.\n\n## Section 4: Docker\n\nIntroduction to Docker to run your REST APIs. We talk about images, containers, and how to run applications.\n\n## Section 5: Flask-Smorest\n\nWe introduce the Flask-Smorest extension, a library that greatly simplifies writing REST APIs using Flask. It also provides things like automated documentation generation.\n\n## Section 6: Flask-SQLAlchemy\n\nThe code in this section extends the previous section by replacing the data storage in Python lists with SQLAlchemy, an ORM (Object-Relational Mapping which simplifies connecting to and interacting with a database.\n\n## Section 7: Many-to-many relationships\n\nIn this section we talk about many-to-many relationships using SQLAlchemy.\n\n## Section 8: Authentication with Flask-JWT-Extended\n\nLearn how to perform user authentication using JWTs and the Flask-JWT-Extended library. Here we talk about access token JWTs, as well as refresh tokens, JWT claims, blocklists, password hashing, and more.\n\n## Section 9: Flask-Migrate\n\nAfter deploying your apps, making changes to the database can be really tricky because you have to log in to the database server and manually update the database tables using SQL commands.\n\nFlask-Migrate and the Alembic libraries simplify this job by creating migration scripts.\n\n## Section 10: Git Crash Course\n\nA quick and intense course on Git and GitHub for code sharing.\n\n## Section 11: Deploying to Render.com\n\nLearn how to get your code running in the cloud and make it publicly accessible. In this section we use Render.com for deployments and we also deploy a PostgreSQL database."
  },
  {
    "path": "dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/docs\"\n    schedule:\n      interval: \"weekly\"\n      # Check for npm updates on Sundays\n      day: \"sunday\"\n    target-branch: \"develop\"\n    # Labels on pull requests for security and version updates\n    labels:\n      - \"npm dependencies\"\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ npm install\n```\n\n### Local Development\n\n```\n$ npm run start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ npm run build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true npm run deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> npm run deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "docs/algolia.config.json",
    "content": "{\n    \"index_name\": \"docusaurus-2\",\n    \"start_urls\": [\n        \"https://rest-apis-flask.teclado.com/\"\n    ],\n    \"sitemap_urls\": [\n        \"https://rest-apis-flask.teclado.com/sitemap.xml\"\n    ],\n    \"sitemap_alternate_links\": true,\n    \"stop_urls\": [\n        \"/tests\"\n    ],\n    \"selectors\": {\n        \"lvl0\": {\n        \"selector\": \"(//ul[contains(@class,'menu__list')]//a[contains(@class, 'menu__link menu__link--sublist menu__link--active')]/text() | //nav[contains(@class, 'navbar')]//a[contains(@class, 'navbar__link--active')]/text())[last()]\",\n        \"type\": \"xpath\",\n        \"global\": true,\n        \"default_value\": \"Documentation\"\n        },\n        \"lvl1\": \"header h1\",\n        \"lvl2\": \"article h2\",\n        \"lvl3\": \"article h3\",\n        \"lvl4\": \"article h4\",\n        \"lvl5\": \"article h5, article td:first-child\",\n        \"lvl6\": \"article h6\",\n        \"text\": \"article p, article pre, article li, article td:last-child\"\n    },\n    \"strip_chars\": \" .,;:#\",\n    \"custom_settings\": {\n        \"separatorsToIndex\": \"_\",\n        \"attributesForFaceting\": [\n        \"language\",\n        \"version\",\n        \"type\",\n        \"docusaurus_tag\"\n        ],\n        \"attributesToRetrieve\": [\n        \"hierarchy\",\n        \"content\",\n        \"anchor\",\n        \"url\",\n        \"url_without_anchor\",\n        \"type\"\n        ]\n    },\n    \"conversation_id\": [\n        \"833762294\"\n    ]\n}"
  },
  {
    "path": "docs/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docs/docs/01_course_intro/02_how_to_install_python/README.md",
    "content": "---\ntitle: How to install Python\ndescription: A brief description of the lecture goes here.\nctslug: how-to-install-python\n---\n\n# How to install Python on your computer\n\nIn this lecture I'll guide you through installing Python on your computer. If have already installed Python, feel free to skip to the next lecture!\n\n## On Windows\n\nTo install Python, download the latest version of Python  from https://www.python.org. At the time of writing, that was Python 3.10.4.\n\n:::caution Add to PATH\nAs you go through the installer, make sure to check \"Add Python to PATH\".\n:::\n\nOnce Python is installed, you can execute the program `cmd.exe`. This is a command-line interface to your computer. Here, just type the word `python` and that will start the Python program.\n\nAt all points during the course, you can always type `python name_of_file.py` and that will execute the code of a file called `name_of_file.py`\n\nIf you have multiple versions of Python installed, such as a version you installed a while ago, you'll need to use the complete path to Python in order to run it. Usually it'll look something like this:\n\n```\nC:\\\\Users\\\\yourname\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python39-32\\\\python.exe\n```\n\nWhen you use an IDE, such as [Visual Studio Code](../how_to_install_ide/), you can use the integrated terminal instead of `cmd.exe`.\n\n## On Mac\n\nTo install Python, download the latest version of Python  from https://www.python.org. At the time of writing, that was Python 3.10.4.\n\nOnce Python is installed, you can execute the program `Terminal.app`. This is a command-line interface to your computer. Here, just type the word `python3.9` and that will start the Python program.\n\nAt all points during the course, you can always type `python3.9 name_of_file.py` and that will execute the code of a file called `name_of_file.py`."
  },
  {
    "path": "docs/docs/01_course_intro/03_how_to_install_ide/README.md",
    "content": "---\ntitle: How to install an IDE\ndescription: What IDE should you use? How do you install it? Let me show you in this quick guide.\nctslug: how-to-install-an-ide\n---\n\n# How to install an IDE\n\nAn IDE is an Integrated Development Environment. If you've got experience coding, I'm sure you've used an IDE at some point or another.\n\nIDEs are text editors that let you modify your code. However, as the name says, they do a bit more than just that.\n\nOften we can use IDEs to run our code, connect to databases, use a debugger, or a whole host of other things!\n\nThroughout this course I use Visual Studio Code. It's a very powerful IDE that you can get for free at https://code.visualstudio.com/. If you get VS Code, I've got a blog post on how to set it up for Python development: https://blog.tecladocode.com/how-to-set-up-visual-studio-code-for-python-development/\n\n## Opening Projects\n\nWhenever you work using an IDE, you should open separate projects in separate windows:\n\n- 👍 When you start a section of the course, make a folder for that section and open it with VSCode. Now VSCode treats that as a \"project\" folder.\n- 👎 Make a folder for the entire course and open it with VSCode. Inside it, make a folder for each section. VSCode will treat the top-level course folder as the \"project\", and your experience will be a bit more difficult.\n\nI've noticed some students like opening their \"projects\" folder with the IDE, so that they can work on all their projects in one window. This is likely to cause problems due to how Python looks for code files to use and import (more on that when you get to the \"Imports\" section of the Python Refresher!).\n\nSo don't be afraid to have many different project folders, each one with their own virtual environment and dependencies. That's normal and will make it much easier to work with!"
  },
  {
    "path": "docs/docs/01_course_intro/04_what_is_rest_api/README.md",
    "content": "---\ntitle: \"What is a REST API?\"\ndescription: \"There's a lot of confusion around what is and isn't a REST API. Let's take a look!\"\nctslug: what-is-an-rest-api\n---\n\n# What is a REST API?\n\n## What is an API?\n\nAPI stands for \"Application Programming Interface\", but that's not an overly helpful name!\n\nThe most important part of the term is \"Interface\". Just as the interface to a car is the parts we humans interact with (steering wheel, pedals, gear stick), the interface to an application is the code that another application can interact with.\n\nThis way, any part of an application that can be \"called\" or \"executed\" from another application, is part of that application's API.\n\nFor example, let's say you make a simple Python library to save data to a database. This is what the library looks like:\n\n```py\ndef save_to_db(what_to_save):\n    pass\n\n\ndef get_from_db(query):\n    pass\n```\n\nAssume that the functions are implemented and they do something!\n\nThis \"library\" has an API: the `save_to_db` and `get_from_db` functions. These are the functions that the library makes available to other programs (or parts of programs), which those other programs should use to save and get data from a database.\n\nIf you look at it this way, almost everything in programming has at least an \"interface\".\n\nAs another example, when you code a class, it has an interface: the public attributes and methods.\n\nSo the key to an API is that it has to be publicly callable, and it allows the _client_ (whoever calls it) to interact with the program that offers the API.\n\n### An API with Flask\n\nWhen we make Flask apps, we also have some public functions that can be called. Our public functions are each associated to an endpoint, such as `/store`.\n\nThat way a client (such as another Python program, or even a web browser) can access the `/store` endpoint of our application, and we can run some code and return a value.\n\nIf our Flask app is hosted at `http://my-flask-app.com`, then accessing `http://my-flask-app.com/store` would execute the function associated with the `/store` endpoint in our app, and the client would receive the data returned by the associated function.\n\nThat data might look like this:\n\n```json\n{\n    \"stores\": [\n        {\n            \"name\": \"My Store\",\n            \"items\": [\n                {\n                    \"name\": \"my item\",\n                    \"price\": 15.99\n                }\n            ]\n        }\n    ]\n}\n```\n\n### The purpose of APIs\n\nWe've learned that we can make a Flask app and expose certain functions to the public by using endpoints. Clients can then make requests (we'll learn how later), and get data.\n\nClients can also send data, which the Flask functions can use.\n\nBut _why_? If you want to use certain functions, why not just code them in your application?\n\nThere's one main reason: so two or more clients can use the API without having to duplicate the logic that the API offers inside their own code.\n\nLet's say you want to build a weather app.\n\nYou could try to install sensors at the top of your house, connect them directly to the computer running your code, and then offer weather info based on what the sensors say...\n\nOr you could request weather data from the OpenWeatherMap API, just as tens of thousands of other devices do.\n\nMuch easier, and all you have to do is make a request to the API!\n\n### What is a client?\n\nAn API client can be any device, such as a web app or a mobile app.\n\n### Making an API for your own consumption\n\nMany software companies make APIs that only they use (so they aren't fully public).\n\nHere's an example. You're making a multiplayer mobile game, and you need to store information about the moves that your character is making.\n\nIn your mobile app code, you could connect to a central database and store the moves there. Apps in other mobile devices would also connect to the central database and store (and read) the moves from there.\n\nBut what happens when you want to expand your app to other devices? Let's say, iOS and Android?\n\nThen you've got to duplicate your database logic in two places: the two app codebases. The problem is compounded if you want to expand to computers, consoles, etc.\n\nIt's easier to have an API which exposes certain functions that let your app save and retrieve data from a database, and have all your devices use that same API.\n\nIt will be much simpler, and when you want to make database changes you most likely won't have to change the code of each mobile app.\n\n## What is REST?\n\nNow that you know what an API is, a slightly more difficult question to answer is \"What is a REST API?\".\n\nA REST API is just an API that follows specific conventions and has specific characteristics.\n\nREST APIs deal in resources, so every individual \"thing\" that can be named is a resource. For example, stores, items, tags, users, or less concrete things like temporal services or collections of other resources.\n\nThe main characteristics (or constraints) of a REST API are:\n\n1. **Uniform interface**. Whichever way clients should access a certain resource should also be the way the access other resources. Clients should have a single way to retrieve resources.\n2. **Client-server**. Clients should know the endpoints of the API, but they should not be coupled to the development of the API. A client or a server may be swapped out for a different implementation without the other noticing.\n3. **Stateless**. The server (API) doesn't store anything about previous client requests. Each client request is treated as a brand new client. If the client needs the server to personalize the response, then the client must send the server whatever information the server needs in order to do so.\n4. **Cacheable**. The client or server must be able to cache the resources returned by the API. This is a very general constraint, but it's an important one.\n5. **Layered system**. REST APIs may be developed as multiple layers, where each layer interacts [only with the layer above and below it](https://excalidraw.com/#json=or3Umoigss4yIeuKg3cO8,qH6uDDCXc7DSjweqNvlmzw).\n\nIf you'd like to read a very complete and exhaustive guide about everything that a REST API is, check out [this guide](https://restfulapi.net/).\n\n## The API we'll build in this course\n\nIn this course we'll build a REST API to expose interactions with stores, items, tags, and users. The API will allow clients to:\n\n- Create and retrieve information about stores.\n- Create, retrieve, search for, update, and delete items in those stores.\n- Create tags and link them to items; and search for items with specific tags.\n- Add user authentication to the client apps using the API.\n\nThe API will have the endpoints shown below.\n\n:::info What do the locks mean?\nIt's usually important in APIs that only certain people have access to certain endpoints. For example, paying customers may have access to certain endpoints while free users may not.\n\nWe'll deal with user authentication in a later section, but that's what the locks (🔒) mean below.\n\n- One 🔒 means the user will need to have authenticated within the last few days to make a request.\n- Two 🔒🔒 means the user will need to have authenticated within the last few minutes to make a request.\n- No locks means anybody can make a request.\n:::\n\n### Users\n\n| Method         | Endpoint          | Description                                           |\n| -------------- | ----------------- | ----------------------------------------------------- |\n| `POST`         | `/register`       | Create user accounts given an `email` and `password`. |\n| `POST`         | `/login`          | Get a JWT given an `email` and `password`.            |\n| 🔒 <br/> `POST` | `/logout`         | Revoke a JWT.                                         |\n| 🔒 <br/> `POST` | `/refresh`        | Get a fresh JWT given a refresh JWT.                  |\n| `GET`          | `/user/{user_id}` | (dev-only) Get info about a user given their ID.      |\n| `DELETE`       | `/user/{user_id}` | (dev-only) Delete a user given their ID.              |\n\n\n### Stores\n\n| Method   | Endpoint      | Description                              |\n| -------- | ------------- | ---------------------------------------- |\n| `GET`    | `/store`      | Get a list of all stores.                |\n| `POST`   | `/store`      | Create a store.                          |\n| `GET`    | `/store/{id}` | Get a single store, given its unique id. |\n| `DELETE` | `/store/{id}` | Delete a store, given its unique id.     |\n\n### Items\n\n| Method           | Endpoint     | Description                                                                                         |\n| ---------------- | ------------ | --------------------------------------------------------------------------------------------------- |\n| 🔒 <br/> `GET`    | `/item`      | Get a list of all items in all stores.                                                              |\n| 🔒🔒 <br/> `POST`  | `/item`      | Create a new item, given its name and price in the body of the request.                             |\n| 🔒 <br/> `GET`    | `/item/{id}` | Get information about a specific item, given its unique id.                                         |\n| `PUT`            | `/item/{id}` | Update an item given its unique id. The item name or price can be given in the body of the request. |\n| 🔒 <br/> `DELETE` | `/item/{id}` | Delete an item given its unique id.                                                                 |\n\n### Tags\n\n| Method   | Endpoint              | Description                                             |\n| -------- | --------------------- | ------------------------------------------------------- |\n| `GET`    | `/store/{id}/tag`     | Get a list of tags in a store.                          |\n| `POST`   | `/store/{id}/tag`     | Create a new tag.                                       |\n| `POST`   | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |\n| `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item.                              |\n| `GET`    | `/tag/{id}`           | Get information about a tag given its unique id.        |\n| `DELETE` | `/tag/{id}`           | Delete a tag, which must have no associated items.      |\n\nAs you can see, we've got a lot to build!\n\nWe'll start building REST APIs in section 3, \"Your first REST API\". Here we'll create a simpler version of the REST API detailed above, without tags or user authentication.\n\nThen, over the following sections, we'll improve on this REST API. We'll add:\n\n- Flask extensions to simplify our code.\n- Use Docker to run the API more reliably.\n- Use PostgreSQL for data storage.\n- Add user authentication.\n- Add item tagging.\n- Add an admin panel so changing data manually is a bit easier.\n- And much more!\n"
  },
  {
    "path": "docs/docs/01_course_intro/_category_.json",
    "content": "{\n    \"label\": \"Course Introduction\",\n    \"position\": 1\n}\n"
  },
  {
    "path": "docs/docs/01_course_intro/index.md",
    "content": "---\nid: intro\n---\n\n# REST APIs with Flask and Python\n\nimport VideoEmbed from \"@site/src/components/VideoEmbed\";\n\n<div style={{ maxWidth: \"720px\", margin: \"3rem auto\", boxShadow: \"0 5px 15px 0 rgba(0, 0, 0, 0.15)\" }}>\n<VideoEmbed url=\"https://customer-zmitazl0ztnd2pvm.cloudflarestream.com/1c4db6119cf0c6e682a88a737af146eb/iframe?poster=https%3A%2F%2Fcustomer-zmitazl0ztnd2pvm.cloudflarestream.com%2F1c4db6119cf0c6e682a88a737af146eb%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600\" />\n</div>\n\nHi, and welcome!\n\nREST APIs with Flask and Python is a complete course that teaches you how to develop complete, professional REST APIs using **Flask**, **PostgreSQL**, and **Docker**.\n\nIn this website you can find the complete course notes and code. We made this to help students navigate the course more easily. Also, every single piece of code we write in the course is in the notes, with brief explanations.\n\nWe've found that really helps review the course content later on!\n\nThis is how we recommend taking the course:\n\n- Start at the first section of the course, and watch the videos. If you're comfortable, watch them in 1.25x or 1.5x speed! This will help you understand the content holistically.\n- Then, re-watch the videos slowly while coding together with me. Write every piece of code by hand, just as you see it in the videos. By the time you're done with a section, after writing all the code, you'll be very comfortable with all the content in it.\n- Move onto the next section, but keep the code from each section in a different folder. That way you can then look at the full project as it was at the end of each section, and that will help you review!\n\nI promise that if you follow this approach, you will master the content of this course within a few weeks. And you'll soon be able to code complete REST APIs using Flask and Python!\n\n:::tip Note-taking not required\nFeel free to take notes while you go through the course, but you don't need to!\n\nThis very website is a perfect set of notes for you to come back to weeks, months, or even years down the line to review what you learned in the course (and, let's be honest, find those snippets of code that you can just copy into your projects).\n:::\n\n## Course Set-up and Housekeeping\n\nBelow are some quick guides to get you started. Feel free to read through them if you need, or skip them!\n\nAfter this, we have a [Python Refresher](../02_python_refresher/index.md). If you are very comfortable with Python, feel free to skip that too!\n\nIf you're skipping the Python Refresher, move onto Your First REST API.\n\nI'll see you there!\n\nimport DocCardList from '@theme/DocCardList';\nimport {useCurrentSidebarCategory} from '@docusaurus/theme-common';\n\n<DocCardList items={useCurrentSidebarCategory().items}/>\n"
  },
  {
    "path": "docs/docs/02_python_refresher/_category_.json",
    "content": "{\n    \"label\": \"Python Refresher\",\n    \"position\": 2\n}\n"
  },
  {
    "path": "docs/docs/02_python_refresher/index.md",
    "content": "---\nname: \"A Complete Python Refresher\"\n---\n\n# Python Refresher\n\nWe've included a Python Refresher section in this course to help you, in case you haven't done any Python for a while or you are coming from a different programming language.\n\nThere are no written notes for this section, but all the videos are available in the Udemy course.\n\nIf you are fully new to programming _and_ Python, this course may be a bit advanced. Feel free to read through out beginner Python e-book anyway, as it might be helpful: [https://python.tecladocode.com](https://python.tecladocode.com).\n"
  },
  {
    "path": "docs/docs/03_first_rest_api/01_project_overview/README.md",
    "content": "---\ntitle: Project Overview\ndescription: A first look at the project we'll build in this section.\nctslug: overview-of-your-first-rest-api\n---\n\n# Overview of your first REST API\n\nimport VideoEmbed from \"@site/src/components/VideoEmbed\";\n\n<div style={{ maxWidth: \"720px\", margin: \"3rem auto\", boxShadow: \"0 5px 15px 0 rgba(0, 0, 0, 0.15)\" }}>\n<VideoEmbed url=\"https://customer-zmitazl0ztnd2pvm.cloudflarestream.com/cda9c0473bdc485a36905144f13f4d3f/iframe?poster=https%3A%2F%2Fcustomer-zmitazl0ztnd2pvm.cloudflarestream.com%2Fcda9c0473bdc485a36905144f13f4d3f%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600\" />\n</div>\n\nIn this section we'll make a simple REST API that allows us to:\n\n- Create stores, each with a `name` and a list of stocked `items`.\n- Create an item within a store, each with a `name` and a `price`.\n- Retrieve a list of all stores and their items.\n- Given its `name`, retrieve an individual store and all its items.\n- Given a store `name`, retrieve only a list of item within it.\n\nThis is how the interaction will go!\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\n## Create stores\n\nRequest:\n\n```\nPOST /store {\"name\": \"My Store\"}\n```\n\nResponse:\n\n```\n{\"name\": \"My Store\", \"items\": []}\n```\n\n## Create items\n\nRequest:\n\n```\nPOST /store/My Store/item {\"name\": \"Chair\", \"price\": 175.50}\n```\n\nResponse:\n\n```\n{\"name\": \"Chair\", \"price\": 175.50}\n```\n\n## Retrieve all stores and their items\n\nRequest:\n\n```\nGET /store\n```\n\nResponse:\n\n```\n{\n    \"stores\": [\n        {\n            \"name\": \"My Store\",\n            \"items\": [\n                {\n                    \"name\": \"Chair\",\n                    \"price\": 175.50\n                }\n            ]\n        }\n    ]\n}\n```\n\n## Get a particular store\n\nRequest:\n\n```\nGET /store/My Store\n```\n\nResponse:\n\n```\n{\n    \"name\": \"My Store\",\n    \"items\": [\n        {\n            \"name\": \"Chair\",\n            \"price\": 175.50\n        }\n    ]\n}\n```\n\n## Get only items in a store\n\nRequest:\n\n```\nGET /store/My Store/item\n```\n\nResponse:\n\n```\n[\n    {\n        \"name\": \"Chair\",\n        \"price\": 175.50\n    }\n]\n```"
  },
  {
    "path": "docs/docs/03_first_rest_api/02_getting_set_up/README.md",
    "content": "---\ntitle: Getting set up\ndescription: Set up a Flask project and create the Flask app.\nctslug: getting-set-up\n---\n\n# Getting set up\n\nimport VideoEmbed from \"@site/src/components/VideoEmbed\";\n\n<div style={{ maxWidth: \"720px\", margin: \"3rem auto\", boxShadow: \"0 5px 15px 0 rgba(0, 0, 0, 0.15)\" }}>\n<VideoEmbed url=\"https://customer-zmitazl0ztnd2pvm.cloudflarestream.com/42b7de55034431b4c4c9420460f8df7d/iframe?poster=https%3A%2F%2Fcustomer-zmitazl0ztnd2pvm.cloudflarestream.com%2F42b7de55034431b4c4c9420460f8df7d%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600\" />\n</div>\n\nCreate a virtual environment and activate it.\n   \n```\npython3.10 -m venv .venv\nsource .venv/bin/activate\n```\n\nInstall Flask.\n   \n```\npip install flask\n```\n\nCreate a file for the Flask app (I like to call it `app.py`)\nCreate the Flask app.\n\n```py title=\"app.py\"\nfrom flask import Flask\n\napp = Flask(__name__)\n```\n\nNow you can run this app using the Flask Command-Line Interface (CLI):\n\n```\nflask run\n```\n\nBut the app doesn't do anything yet! Let's work on our first API endpoint next."
  },
  {
    "path": "docs/docs/03_first_rest_api/03_first_rest_api_endpoint/README.md",
    "content": "---\ntitle: Your First REST API Endpoint\ndescription: Learn how to define a REST API endpoint using Flask.\nctslug: your-first-rest-api-endpoint\n---\n\n# Your First REST API Endpoint\n\nimport LockedVideoEmbed from \"@site/src/components/LockedVideoEmbed\";\n\n<LockedVideoEmbed />\n\nLet's start off by defining where we'll store our data. In most REST APIs, you'd store your data in a database. For now, and for simplicity, we'll store it in a Python list.\n\nLater on we'll work on making this data dynamic. For now let's use some sample data.\n\n```py title=\"app.py\"\nfrom flask import Flask\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"my item\", \"price\": 15.99}]}]\n```\n\nNow that we've got the data stored, we can go ahead and make a Flask route that, when accessed, will return all our data.\n\n```py title=\"app.py\"\nfrom flask import Flask\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"my item\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n```\n\n## Anatomy of a Flask route\n\nThere are two parts to a Flask route:\n\n- The endpoint decorator\n- The function that should run\n\nThe endpoint decorator (`@app.get(\"/store\")`) _registers_ the route's endpoint with Flask. That's the `/store` bit. That way, the Flask app knows that when it receives a request for `/store`, it should run the function.\n\nThe function's job is to do everything that it should, and at the end return _something_. In most REST APIs, we return JSON, but you can return anything that can be represented as text (e.g. XML, HTML, YAML, plain text, or almost anything else)."
  },
  {
    "path": "docs/docs/03_first_rest_api/04_what_is_json/README.md",
    "content": "---\ntitle: \"What is JSON?\"\ndescription: JSON is the way we normally transfer data to and from REST APIs.\nctslug: what-is-json\n---\n\n# What is JSON?\n\nJSON is just a (usually long) string whose contents follow a specific format.\n\nOne example of JSON:\n\n```json\n{\n    \"key\": \"value\",\n    \"another\": 25,\n    \"listic_data\": [\n        1,\n        3,\n        7\n    ],\n    \"sub_objects\": {\n        \"name\": \"Rolf\",\n        \"age\": 25\n    }\n}\n```\n\nSo at its core, you've got:\n\n- Strings\n- Numbers\n- Booleans (`true` or `false`)\n- Lists\n- Objects (akin to dictionaries in Python)\n  - Note that objects are not ordered, so the keys could come back in any order. This is not a problem!\n\nAt the top level of a piece of JSON you can have an object or a list. So this is also valid JSON:\n\n```json\n[\n    {\n        \"name\": \"Rolf\",\n        \"age\": 25\n    },\n    {\n        \"name\": \"Anne\",\n        \"age\": 27\n    },\n    {\n        \"name\": \"Adam\",\n        \"age\": 23\n    }\n]\n```\n\nWhen we return a Python dictionary in a Flask route, Flask automatically turns it into JSON for us, so we don't have to.\n\nRemember that \"turning it into JSON\" means two things:\n\n1. Change Python keywords and values so they match the JSON standard (e.g. `True` to `true`).\n2. Turn the whole thing into a single string that our API can return.\n\n:::tip\nNote that JSON can be \"prettified\" (as the above examples), although usually it is returned by our API \"not-prettified\":\n\n```json\n[{\"name\":\"Rolf\",\"age\":25},{\"name\":\"Anne\",\"age\":27},{\"name\":\"Adam\",\"age\":23}]\n```\n\nThis removal of newlines and spaces, believe it or not, adds up and can save a lot of bandwidth since there is less data to transfer between the API server and the client.\n:::"
  },
  {
    "path": "docs/docs/03_first_rest_api/05_make_request_to_rest_api/README.md",
    "content": "---\ntitle: How to interact with your REST API\ndescription: Use Postman and Insomnia REST Client to interact with your REST API.\nctslug: how-to-interact-with-your-rest-api\n---\n\n# How to make a request to a REST API\n\nOne of the most important things about any software development is to make sure that our projects work!\n\nSo we need to be able to test our project, run it, and make sure it does what we think it does.\n\nThere are two main ways of doing this:\n\n- With automated tests.\n- With manual, exploratory testing.\n\nUsually you'd go with exploratory first, and then you'd make automated tests based on your manual tests.\n\nIn this course we won't cover automated testing of your REST API (it's a long topic, and we've got another course on it).\n\nHowever, we will cover a lot of things you can do with manual testing.\n\nThere are two tools I use for exploratory testing: Postman and Insomnia. It's up to you which one to use, but if you haven't used either one before, I recommend Insomnia.\n\nIt's a bit easier to get started with, and it's free and open source.\n\nStart by [downloading Insomnia REST Client](https://insomnia.rest/).\n\nOnce you've opened it, create a Project. I would call it \"REST APIs with Flask and Python\".\n\n![Creating the Project for this course](https://res.cloudinary.com/teclado/image/upload/v1689180715/courses/rest-apis-flask-python/creating-project_qsyxlg.png)\n\nThen, create a new Request Collection. Call it \"Stores REST API\".\n\n![Creating the Stores REST API Request Collection](https://res.cloudinary.com/teclado/image/upload/v1689180710/courses/rest-apis-flask-python/making-request-collection_lcthlv.png)\n\nIn the Request Collection, we can now add requests! Each request has a few parts:\n\n- A **method**, such as `GET` or `POST`. The method is just a piece of data sent to the server, but _usually_ certain methods are used for certain things.\n- The **URL** that you want to request. For our API, this is formed of the \"Base URL\" (for Flask apps, that's `http://127.0.0.1:5000`), and the endpoint (e.g. `/store`).\n- The **body**, or any data that you want to send in the request. For example, when creating stores or items we might send some data.\n- The **headers**, which are other pieces of data with specific names, that the server can use. For example, a header might be sent to help the server understand _who_ is making the request.\n\nLet's create our first request, `GET /store`.\n\nMake a new request using the Insomnia interface. First, use the dropdown to start:\n\n![How to make a request using the Insomnia interface](https://res.cloudinary.com/teclado/image/upload/v1689180711/courses/rest-apis-flask-python/making-request_hmiptl.png)\n\nThen enter the request name. Leave the method as `GET`:\n\n![Enter the request name and method](https://res.cloudinary.com/teclado/image/upload/v1689180712/courses/rest-apis-flask-python/set-request-name-and-method_bc6smy.png)\n\nOnce you're done, you will see your request in the collection:\n\n![The request is shown in the collection](https://res.cloudinary.com/teclado/image/upload/v1689180711/courses/rest-apis-flask-python/before-setting-url_qjxvyr.png)\n\nNext up, enter the URL for your request. Here we will be requesting the `/store` endpoint. Remember to include your Base URL as well:\n\n![Entering the full URL for the request in Insomnia](https://res.cloudinary.com/teclado/image/upload/v1689180714/courses/rest-apis-flask-python/url-set_fgp9s9.png)\n\nOnce you're done, make sure that your Flask app is running! If it isn't, remember to activate your virtual environment first and then run the app:\n\n```\nsource .venv/bin/activate\nflask run\n```\n\n:::caution\nThe Flask app will run, by default, on port 5000. If you have another (or the same) app already running, you'll get an error because the port will be \"busy\".\n\nIf you get an error, read it carefully and make sure that no other Flask app is running on the same port.\n:::\n\nOnce your Flask app is running, you can hit \"Send\" on the Insomnia client, and you should see the JSON come back from your API!\n\n![Making a request to our API using Insomnia](https://res.cloudinary.com/teclado/image/upload/v1689180712/courses/rest-apis-flask-python/after-pressing-send_okjkjq.png)\n\nIf that worked and you can see your JSON, you're good to go! You've made your first API request. Now we can continue developing our REST API, remembering to always create new Requests in Insomnia and test our code as we go along!"
  },
  {
    "path": "docs/docs/03_first_rest_api/06_creating_stores/README.md",
    "content": "---\ntitle: How to create stores\ndescription: Learn how to add data to our REST API.\nctslug: how-to-create-stores\n---\n\n# How to create stores in our REST API\n\nTo create a store, we'll receive JSON from our client (in our case, Insomnia, but it could be another Python app, JavaScript, or any other language or tool).\n\nOur client will send us the name of the store they want to create, and we will add it to the database!\n\nFor this, we will use a `POST` method. `POST` is usually used to receive data from clients and either use it, or create resources with it.\n\nIn order to access the JSON body of a request, we will need to import `request` from `flask`. Your import list should now look like this:\n\n```py\nfrom flask import Flask, request\n```\n\nThen, create your endpoint:\n\n```py title=\"app.py\"\n# highlight-start\nfrom flask import Flask, request\n# highlight-end\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"my item\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n# highlight-start\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n# highlight-end\n```\n\nHere we use `request.get_json()` to retrieve the JSON content of our request.\n\nThen we create a new dictionary that represents our store. It has a `name` and `items` (which is an empty list).\n\nThen we append this store to our `stores` list.\n\nFinally we return the newly created `store`. It's empty, but it serves as a **success message**, to tell our client that we have successfully created what they wanted us to create.\n\n:::tip Returning a status code\nEvery response has a status code, which tells the client if the server was successful or not. You already know at least one status code: 404. This means \"Not found\".\n\nThe most common status code is `200`, which means \"OK\". That's what Flask returns by default, such as in the `get_stores()` function.\n\nIf we want to return a different status code using Flask, we can put it as the second value returned by an endpoint function. In `create_store()`, we are returning the code `201`, which means \"Created\".\n:::"
  },
  {
    "path": "docs/docs/03_first_rest_api/07_creating_items/README.md",
    "content": "---\ntitle: How to create items in each store\ndescription: A brief description of the lecture goes here.\nctslug: how-to-create-items-in-each-store\n---\n\n# How to create items in our REST API\n\nNext up, let's work on adding items to a store!\n\nHere's how that's going to work:\n\n1. The client will send us the store name where they want their new item to go.\n2. They will also send us the name and price of the new item.\n3. We'll go through the stores one at a time, until we find the correct one (whose name matches what the user gave us).\n4. We'll append a new item dictionary to that store's `items`.\n\n## URL parameters\n\nThere are a few ways for clients to send us data. So far, we've seen that clients can send us JSON.\n\nBut data can be included in a few other places:\n\n- The body (as JSON, form data, plain text, or a variety of other formats).\n- Inside the URL, part of it can be dynamic.\n- At the end of the URL, as _query string arguments_.\n- In the request headers.\n\nFor this request, the client will send us data in two of these at the same time: the body and the URL.\n\nHow does a dynamic URL look like?\n\nHere's a couple examples:\n\n- `/store/My Store/item`\n- `/store/another-store/item`\n- `/store/a/item`\n\nIn those three URLs, the \"store name\" was:\n\n- `My Store`\n- `another-store`\n- `a`\n\nWe can use Flask to define dynamic endpoints for our routes, and then we can grab the value that the client put inside the URL.\n\nThis allows us to make URLs that make interacting with them more natural.\n\nFor example, it's nicer to make an item by going to `POST /store/My Store/item`, rather than going to `POST /add-item` and then pass in the store name in the JSON body.\n\nTo create a dynamic endpoint for our route, we do this:\n\n```py\n@app.route(\"/store/<string:name>/item\")\n```\n\nThat makes it so the route function will use a `name` parameter whose value will be what the client put in that part of the URL.\n\nWithout further ado, let's make our route for creating items within a store!\n\n```py title=\"app.py\"\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"my item\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n# highlight-start\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item\n    return {\"message\": \"Store not found\"}, 404\n# highlight-end\n```\n\n:::tip Not the most efficient way\nIn this endpoint we're iterating over all stores in our list until we find the right one. This is very inefficient, but we'll look at better ways to do this kind of thing when we look at databases.\n\nFor now, focus on Flask, and don't worry about efficiency of our code!\n:::"
  },
  {
    "path": "docs/docs/03_first_rest_api/08_return_data_from_rest_api/README.md",
    "content": "---\ntitle: Get a specific store and its items\ndescription: How to use Flask to return data from your REST API to your client.\nctslug: get-a-specific-store-and-its-items\n---\n\n# How to get a specific store and its items\n\nThe last thing we want to look at in our first REST API is returning data that uses some filtering.\n\nUsing URL parameters, we can select a specific store:\n\n```py\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n```\n\nAnd just as we did when creating an item in a store, you can use the same endpoint (with a `GET` method), to select the items in a store:\n\n```py\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n```\n"
  },
  {
    "path": "docs/docs/03_first_rest_api/09_final_code/README.md",
    "content": "---\ntitle: Final code of this section\ndescription: Overview of the project we've built and all the code in it.\nctslug: final-code-of-this-section\n---\n\n# Final code of this section\n\nHere's everything we've written in this section!\n\n```py title=\"app.py\"\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [\n    {\n        \"name\": \"My Store\",\n        \"items\": [\n            {\n                \"name\": \"Chair\",\n                \"price\": 15.99\n            }\n        ]\n    }\n]\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n```"
  },
  {
    "path": "docs/docs/03_first_rest_api/09_final_code/end/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/03_first_rest_api/Insomnia_section3.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:36:47.360Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_e15dafc098ac4a2198304d2aead2a5b9\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900815265,\"created\":1666123912423,\"url\":\"http://127.0.0.1:5000/store/My Store/item\",\"name\":\"/store/<name>/item Create item in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Table\\\",\\n\\t\\\"price\\\": 17.99\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423081,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666124528874,\"created\":1666124528874,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528874,\"_type\":\"request_group\"},{\"_id\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"parentId\":null,\"modified\":1666991857781,\"created\":1666122928011,\"name\":\"Section 3\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_697ca0714a3d4e94819411e3df0a2a17\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900846590,\"created\":1666124316888,\"url\":\"http://127.0.0.1:5000/store/My store3/item\",\"name\":\"/store/<name>/item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423056,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9451df3aae714e93a8ed529b3a1f99c2\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666124555354,\"created\":1666122990495,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423031,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6363c8d4deb74b5bbccb1e2105277dac\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900812784,\"created\":1666124168137,\"url\":\"http://127.0.0.1:5000/store/My store3\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124422956,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0a9c4822679b4eae92dec7432fe144b8\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900810115,\"created\":1666123651275,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store3\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124422881,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_19db457230041d88ca9420d1b3c0f1f02bbcae93\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928025,\"created\":1666122928025,\"name\":\"Base Environment\",\"data\":{},\"dataPropertyOrder\":null,\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_19db457230041d88ca9420d1b3c0f1f02bbcae93\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928027,\"created\":1666122928027,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_c5b803a7c6514ff29573e26487d898d4\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928018,\"created\":1666122928018,\"fileName\":\"Your First REST API\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/03_first_rest_api/_category_.json",
    "content": "{\n    \"label\": \"Your First REST API\",\n    \"position\": 3\n}\n"
  },
  {
    "path": "docs/docs/04_docker_intro/01_what_is_docker_container/README.md",
    "content": "---\nctslug: what-is-a-docker-container\ndescription: Learn what Docker images and containers are, and how we can use them to distribute and run our applications.\n---\n\n# What are Docker images and containers?\n\nI'm sure you have heard of the term \"Virtual Machine\". A virtual machine is an emulation of an Operating System. For example, if you run a Windows virtual machine on your MacOS computer, it will run a whole copy of Windows so you can run Windows programs.\n\nThis diagram shows what happens in that case:\n\n![Virtual Machine Diagram stack](https://res.cloudinary.com/teclado/image/upload/v1689180716/courses/rest-apis-flask-python/vm.drawio_nlrxmx.png)\n\nWhen you run a Virtual Machine, you can configure what hardware it has access to (e.g. 50% of the host's RAM, 2 CPU cores, etc).\n\nDocker containers are a bit different because they don't emulate an Operating System. They use the Operating System kernel of your computer, and run as a process within the host.\n\nContainers have their own storage and networking, but because they don't have to emulate the operating system and everything that entails, they are much more lightweight.\n\nThis diagram shows how Linux containers run in a Linux host:\n\n![Docker Diagram stack](https://res.cloudinary.com/teclado/image/upload/v1689180716/courses/rest-apis-flask-python/docker-linux.drawio_ebvff5.png)\n\nLooks similar, but the `docker -> container` section is much more efficient than running a VM because it **uses the host's kernel** instead of running its own.\n\n## What is a kernel? 🍿\n\nAn Operating System is made up of two main parts:\n\n- The **kernel**\n- Files and programs that come with the operating system\n\nThe Linux kernel, for example, is used by all Linux Operating Systems (like Ubuntu, Fedora, Debian, etc.).\n\n:::caution\nSince containers use the host's kernel, you can't run a Windows Docker container natively in a MacOS host. Similarly, you can't run a Linux container natively on Windows or MacOS hosts.\n:::\n\n## How to run Linux containers on Windows or MacOS?\n\nWhen you use Docker Desktop (which I'll show you in the next lecture), it runs a Linux Virtual Machine for you, which then is used to run your Linux containers.\n\nBut aren't you then doing this?\n\n```\nhardware -> macos -> hypervisor -> linux vm -> docker -> container -> container program\n```\n\nAnd isn't that much less efficient than just running the program in a Linux virtual machine?\n\nYes. Running Linux containers on MacOS or Windows is \"worse\" than just running the programs in a Linux VM. However, **99% of the time, you will be running Linux containers in a Linux host, which is much more efficient**.\n\n:::tip Why do we always run Linux containers in a Linux host?\nWhen you want to deploy your applications to share them with your users, you will almost always be running your app in a Linux server (provided by a _deployment company_, more on that later). There are a few reasons for this. Among them, Linux is free!\n:::\n\n## Why are containers more efficient than VMs?\n\nFrom now on let's assume we are running native Linux containers in a Linux host, as that is by far the most common thing to do!\n\nWhen you run a VM, it runs the entire operating system. However, when you run a container it uses part of the host's Operating System (called the kernel). Since the kernel is already running anyway, there is much less work for Docker to do.\n\nAs a result, containers start up faster, use fewer resources, and need much less hard disk space to run.\n\n## Can you run an Ubuntu image when the host is Linux but not Ubuntu?\n\nSince the Linux kernel is the same between distributions, and since Docker containers only use the host's kernel, it doesn't matter which distribution you are running as a host. You can run containers of any distribution with any other distribution as a host.\n\n## How many containers can you run at once?\n\nEach container uses layers to specify what files and programs they need. For example, if you run two containers which both use the same version of Python, you'll actually only need to store that Python executable once. Docker will take care of sharing the data between containers.\n\nThis is why you can run many hundreds of containers in a single host, because there is less duplication of files they use compared to virtual machines.\n\n## What does a Docker container run?\n\nIf you want to run your Flask app in a Docker container, you need to get (or create) a Docker image that has all the dependencies your Flask app uses, except from the OS kernel:\n\n- Python\n- Dependencies from `requirements.txt`\n- Possibly `nginx` or `gunicorn` (more on this when we talk about deployment)\n\n:::info Aren't there more dependencies?\nThe keen-eyed among you may be thinking: if all you have is the kernel and nothing else, aren't there more dependencies?\n\nFor example, Python _needs_ the C programming language to run. So shouldn't we need C in our container also?\n\nYes!\n\nWhen we build our Docker image, we will be building it _on top of_ other, pre-built, existing images. Those images come with the lower-level requirements such as compilers, the C language, and most utilities and programs we need.\n:::\n\n## What is a Docker image?\n\nA Docker image is a snapshot of source code, libraries, dependencies, tools, and everything else (except the Operating System kernel!) that a container needs to run.\n\nThere are many pre-built images that you can use. For example, some come with Ubuntu (a Linux distribution). Others come with Ubuntu and Python already installed. You can also make your own images that already have Flask installed (as well as other dependencies your app needs).\n\n:::info Comes with Ubuntu?\nIn the last lecture I mentioned that Docker containers use the host OS kernel, so why does the container need Ubuntu?\n\nRemember that operating systems are kernel + programs/libraries. Although the container uses the host kernel, we may still need a lot of programs/libraries that Ubuntu ships with. An example might be a C language compiler!\n:::\n\nThis is how you define a Docker image. I'll guide you through how to do this in the next lecture, but bear with me for a second:\n\n```dockerfile\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\nThis is a `Dockerfile`, a definition of how to create a Docker image. Once you have this file, you can ask Docker to create the Docker image. Then, after creating the Docker image, you can ask Docker to run it as a container.\n\n```\nDockerfile ---build--> docker image ---run--> docker container\n```\n\nIn this `Dockerfile` you can see the first line: `FROM python:3.10`. This tells Docker to first download the `python:3.10` image (an image someone else already created), and once that image is created, run the following commands.\n\n:::info What's in the Python image?\nThe `python:3.10` image is also built using a `Dockerfile`! You can see the `Dockerfile` for it [here](https://github.com/docker-library/python/blob/master/3.10/bookworm/Dockerfile).\n\nYou can see it comes `FROM` another image. There is usually a chain of these, images built upon other images, until you reach the base image. In this case, the [base image](https://github.com/docker-library/buildpack-deps/blob/master/debian/bookworm/Dockerfile) is running Debian (a Linux distribution).\n\n<details>\n<summary>Where is the base image!?</summary>\n<div>\n<div>\n\nIf you really want to go deep, you will be able to find...\n\n- The [`python3.10:bookworm`](https://github.com/docker-library/python/blob/master/3.10/bookworm/Dockerfile) image builds on `buildpack-deps:bookworm`\n- [`buildpack-deps:bookworm`](https://github.com/docker-library/buildpack-deps/blob/master/debian/bookworm/Dockerfile) builds on `buildpack-deps:bookworm-scm`\n- [`buildpack-deps:bookworm-scm`](https://github.com/docker-library/buildpack-deps/blob/master/debian/bookworm/scm/Dockerfile) builds on `buildpack-deps:bookworm-curl`\n- [`buildpack-deps:bookworm-curl`](https://github.com/docker-library/buildpack-deps/blob/master/debian/bookworm/curl/Dockerfile) builds on `debian:bookworm`\n- [`debian:bookworm`](https://github.com/debuerreotype/docker-debian-artifacts/blob/f7257ef5b83f6b64385edddeae2d2ba7d1b34935/bookworm/Dockerfile) looks really weird!\n\nEventually, the base image has to physically include the files that make up the operating system. In that last image, that's the Debian OS files that the maintainers have deemed necessary for the `bookworm` image.\n\n</div>\n</div>\n</details>\n\nSo, why the chain?\n\nThree main reasons:\n\n1. So you don't have to write a super long and complex `Dockerfile` which contains everything you need.\n2. So pre-published images can be shared online, and all you have to do is download them.\n3. So when your own images use the same base image, Docker in your computer only downloads the base image once, saving you a lot of disk space.\n:::\n\nBack to our `Dockerfile`. The commands after `FROM...` are specific to our use case, and do things like install requirements, copy our source code into the image, and tell Docker what command to run when we start a container from this image.\n\nThis separation between images and containers is interesting because once the image is created you can ship it across the internet and:\n\n- Share it with other developers.\n- Deploy it to servers.\n\nPlus once you've downloaded the image (which can take a while), starting a container from it is almost instant since there's very little work to do.\n"
  },
  {
    "path": "docs/docs/04_docker_intro/02_run_docker_container/README.md",
    "content": "---\nctslug: how-to-run-a-docker-container\ndescription: Learn how to run a Docker container with your REST API using Docker Desktop.\n---\n\n# How to run a Docker container\n\n## Install Docker Desktop\n\nDocker Desktop is an application to help you manage your images and containers. Download it and install it here: [https://www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/).\n\n## Create your Docker image\n\nNext, download the REST API code from Section 3. You can download it [here](https://www.dropbox.com/s/qs28amk2h420f2y/s03-final-code.zip?dl=0).\n\nIf you want to use the code you wrote while following the videos, that's fine! Just make sure it works by running the Flask app locally and testing it with Insomnia REST Client or Postman.\n\n### Write the `Dockerfile`\n\nIn your project folder (i.e. the same folder as `app.py`), we're going to write the Dockerfile.\n\nTo do this, make a file called `Dockerfile`.\n\n:::caution\nMake sure the file is called `Dockerfile`, and not `Dockerfile.txt` or anything like that!\n:::\n\nInside the `Dockerfile` we're going to write this:\n\n```dockerfile\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\nHere's a quick breakdown of what each line does:\n\n1. `FROM python:3.10` uses the `python:3.10` image as a base.\n2. `EXPOSE 5000` is basically documentation[^1]. It tells the user of the Dockerfile that port 5000 is something the running container will use.\n3. `WORKDIR /app` does it so everything we do in the Docker image will happen in the image's `/app` directory.\n4. `RUN pip install flask` runs a command in the image. Here the command is `pip install flask`, which is what we need to run our app.\n5. `COPY . .` is a bit cryptic! It copies everything in the current folder (so `app.py`) into the image's current folder (so `/app`).\n6. `CMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]` tells the image what command to run when you start a container. Here the command is `flask run --host=0.0.0.0`.\n\n:::tip\nWe need `--host=0.0.0.0` to make Docker be able to do port forwarding, as otherwise the Flask app will only be accessible within the container, but not outside the container.\n:::\n\nNow we need to create the Docker image.\n\nWe do this with the `docker build` command in the terminal.\n\n:::caution\nMake sure to restart your terminal after installing Docker Desktop, so that you have access to the `docker` program in your terminal.\n:::\n\nOpen a terminal (in VSCode that's CMD+J or CTRL+J), and run this command:\n\n```\ndocker build -t rest-apis-flask-python .\n```\n\nThe `-t rest-apis-flask-python` flag is optional, but tags the image, giving it a name. It can be handy for later! The final `.` at the end of the command is not a mistake; it tells the command _what_ to build. The `.` means \"the current directory\".\n\nThis command can take a while to run as it needs to download the `python:3.10` image first. You should see quite a lot of output while the command runs.\n\nWhen the command is finished, you should see this (among other things):\n\n```\n => [2/4] WORKDIR /app                                                                             0.4s\n => [3/4] RUN pip install flask                                                                    2.9s\n => [4/4] COPY . .                                                                                 0.0s\n => exporting to image                                                                             0.1s\n => => exporting layers                                                                            0.1s\n => => writing image sha256:d9a68a03f868e74bca48567dfc9a0b702d1618941a71b77de12ff14e908ba155       0.0s\n => => naming to docker.io/library/rest-apis-first-rest-api                                        0.0s\n```\n\nAnd now your image is built! You should be able to see it in the \"Images\" section of your Docker Desktop app.\n\n## Run the Docker container\n\nWhen we start a Docker container from this image, it will run the `flask run` command. Remember that by default, `flask run` starts a Flask app using port 5000.\n\nBut the container's ports are not accessible from outside the container by default. We need to tell Docker that when we access a certain port in our computer, those requests and responses should be forwarded to a certain port in our container.\n\nSo we'll run the container, but we must remember to forward a port (e.g. 5000) in our computer to port 5000 in the container\n\nTo do so, run this command:\n\n```\ndocker run -d -p 5000:5000 rest-apis-flask-python\n```\n\nWe're passing a few things to `docker run`:\n\n1. `-d` runs the container in the background, so that you can close the terminal and the container keeps running.\n2. `-p 5000:5000` maps port 5000 in your computer to port 5000 in the container.\n3. `rest-apis-flask-python` is the image tag that you want to run.\n\nYou should see something like this as your output:\n\n```\n9f3c564ac64a1723069dda0e80becb70d3697d4bfcbcb626cd5add0c65df173f\n```\n\nThat's the ID of the container. If you're not using Docker Desktop, you need this ID in order to stop the container later (with `docker rm 9f3c564`, that's the first few characters of the ID).\n\nAnd now, if everything has worked, you should be able to access the Flask app _just as if it was running without Docker_!\n\n:::caution Did something not work?\nA common error can happen when the port that you tried to forward isn't available (e.g. because something else is already running):\n\n```\ndocker: Error response from daemon: driver failed programming external connectivity on endpoint bold_goldwasser (ff58b1755c1d1d0fd6b1dd4f59ab3b903b0e68f320624c4a2495672a735039d5): Bind for 0.0.0.0:5000 failed: port is already allocated.\n```\n\nYou have two options: either figure out what is running on port 5000 and shut it down before trying again, or you can change the port that you want to use in your computer:\n\n```\ndocker run -dp 5001:5000 rest-apis-flask-python\n```\n:::\n\nTry making requests using the URL `127.0.0.1:5000` with Insomnia REST Client or Postman, and you should see it working well!\n\n![Insomnia REST Client successfully made a request to the API running in Docker](https://res.cloudinary.com/teclado/image/upload/v1689180719/courses/rest-apis-flask-python/running-app-docker_mkosjm.png)\n\n[^1]: [Docker `EXPOSE` command (Official Documentation)](https://docs.docker.com/engine/reference/builder/#expose)"
  },
  {
    "path": "docs/docs/04_docker_intro/02_run_docker_container/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/04_docker_intro/02_run_docker_container/end/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/04_docker_intro/02_run_docker_container/start/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/04_docker_intro/03_in_depth_docker_tutorial/README.md",
    "content": "---\nctslug: in-depth-docker-tutorial\ndescription: My notes from the official Docker tutorial.\n---\n\n# In-depth Docker Tutorial\n\nLike I mentioned earlier on in this section, this course is not a Docker course!\n\nYou can access the official Docker tutorial (which is free and great) by running the tutorial image:\n\n```\ndocker run -dp 80:80 docker/getting-started\n```\n\nThen you can access http://127.0.0.1/tutorial to launch the official tutorial.\n\nI recommend going through this (although it uses NodeJS as an example 🤮), as it teaches you quite a few important commands and concepts, such as working with volumes and layers.\n\nWhen I went through the official tutorial I took some notes, which you can see below. Remember these may differ from the official tutorial as the Docker team updates the tutorial regularly.\n\nI hope the notes are helpful as a bit of a cheatsheet, but it doesn't beat going through the tutorial yourself and taking your own notes!\n\n---\n\n## How to write a simple Dockerfile for a Node app\n\n```dockerfile\nFROM node:12-alpine\n# Adding build tools to make yarn install work on Apple silicon / arm64 machines\nRUN apk add --no-cache python2 g++ make\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"node\", \"src/index.js\"]\n```\n\nThen build the image into a new container (the `.` below refers to the current directory, where Docker should find the `Dockerfile`). Optionally tag it:\n\n```\ndocker build -t docker-image-tag .\n```\n\n## How to run Docker as a daemon (background)\n\nThis prints out the container ID and runs it in the background.\n\n```\ndocker run -d docker-image-tag\n```\n\n## How to map ports from host machine to Docker container\n\nThis binds port 5000 of the Docker image to port 3000 of the host machine. This way you when you access `127.0.0.1:3000` with your browser, you'll access whatever the Docker image is serving in port `5000`.\n\nDocs: https://docs.docker.com/engine/reference/commandline/run/#publish-or-expose-port--p---expose\n\n```\ndocker run -p 127.0.0.1:3000:5000 docker-image-tag\n```\n\n## Working with Docker volumes\n\nIn a Docker volume, the Docker container can store data in the Docker container's filesystem, and it is actually stored in the volume (which is a location in the host machine).\n\nThis is in contrast to a Bind Mount, which is another type of volume where the Docker container reads files (i.e. is provided files to read) from the host machine. The Docker container cannot modify those files when using Bind Mounts.\n\n| Feature                                      | Named Volumes               | Bind Mounts                     |\n| -------------------------------------------- | --------------------------- | ------------------------------- |\n| Host Location                                | Docker chooses              | You control                     |\n| Mount Example (using `v`)                    | `my-volume:/usr/local/data` | `/path/to/data:/usr/local/data` |\n| Populates new volume with container contents | Yes                         | No                              |\n| Supports Volume Drivers                      | Yes                         | No                              |\n\n### How to map a Named Volume from host to Docker container\n\n```\ndocker run -v volume-name:/path/in/docker/image container-tag\n```\n\nFor example for an app that needs port mapping and a volume:\n\n```\ndocker run -dp 3000:3000 -v todo-db:/etc/todos getting-started\n```\n\nAnd to see _where_ in the host machine the data is actually stored:\n\n```\ndocker volume inspect volume-name\n```\n\n> While running in Docker Desktop, the Docker commands are actually running inside a small VM on your machine. If you wanted to look at the actual contents of the Mountpoint directory, you would need to first get inside of the VM.\n\n### How to use a Bind Mount to provide your app code to a Docker container\n\n```\ndocker run -dp 3000:3000 \\\n    -w /app -v \"$(pwd):/app\" \\\n    node:12-alpine \\\n    sh -c \"apk add --no-cache python2 g++ make && yarn install && yarn run dev\"\n```\n\n-   `-dp 3000:3000` - same as before. Run in detached (background) mode and create a port mapping\n-   `-w /app` - sets the container's present working directory where the command will run from\n-   `-v \"$(pwd):/app\"` - bind mount (link) the host's present `getting-started/app` directory to the container's `/app` directory. Note: Docker requires absolute paths for binding mounts, so in this example we use `pwd` for printing the absolute path of the working directory, i.e. the `app` directory, instead of typing it manually\n-   `node:12-alpine` - the image to use. Note that this is the base image for our app from the Dockerfile\n-   `sh -c \"yarn install && yarn run dev\"` - the command. We're starting a shell using `sh` (alpine doesn't have `bash`) and running `yarn install` to install _all_ dependencies and then running `yarn run dev`. If we look in the `package.json`, we'll see that the `dev` script is starting `nodemon`.\n\nNote that most of this is identical to the `Dockerfile` that you would create for your project. The only difference is the `-v \"$(pwd):/app\"` flag.\n\n## How to pass environment variables to a container\n\nUse the `-e ENV_NAME=env_value` flag with `docker run`.\n\n:::caution Secrets in environment variables\nPassing secrets like database connection strings or API keys to Docker containers can be done with environment variables, but it isn't the most secure way (the official Docker tutorial [will tell you more](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/)).\n\nInstead a better option is to use your orchestration framework's secrets management system (that's a mouthful). The two major options are [Kubernetes](https://kubernetes.io/docs/concepts/configuration/secret/) and [Swarm](https://docs.docker.com/engine/swarm/secrets/), and each have their own secrets management system. More info on this later on!\n:::\n\n## Networking between two containers\n\nFirst create a network with:\n\n```\ndocker network create network-name\n```\n\nThen pass the `--network network-name` flag to `docker run`.\n\nYou can also pass `--network-alias` to `docker run` to give the container you are running a DNS name within the network.\n\nThen create your containers and pass the network to them. For example, this starts up a MySQL image on `linux/amd64`. It also creates a volume and passes in two environment variables which the image uses for configuring MySQL:\n\n```\ndocker run -d \\\n    --network network-name --network-alias mysql --platform linux/amd64 \\\n    -v todo-mysql-data:/var/lib/mysql \\\n    -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_root_password \\\n    -e MYSQL_DATABASE=todos \\\n    mysql:5.7\n```\n\nThen you could run another container on the same network:\n\n```\ndocker run -dp 3000:3000 \\\n  -w /app -v \"$(pwd):/app\" \\\n  --network network-name \\\n  -e MYSQL_HOST=mysql \\\n  -e MYSQL_USER=root \\\n  -e MYSQL_PASSWORD_FILE=/run/secrets/mysql_password \\\n  -e MYSQL_DB=todos \\\n  node:12-alpine \\\n  sh -c \"npm install && npm run dev\"\n```\n\n:::caution\nIn these I'm not passing the MySQL password directly as an environment variable. Instead, I'm passing the path to a file that contains the password.\n\nThat file is created by your Docker orchestration framework's secrets management system. That's a mouthful to say: you define the secret in your orchestration framework, and the framework creates a file which contains the password. That way, the password isn't stored in the environment which is a bit unsafe.\n\nYour application (or, in this case, MySQL), would have to read the contents of the image to find the password.\n\nMore info on this when we learn about deploying our app in production!\n:::\n\n## How to run multiple containers using Docker Compose\n\n1. Create a `docker-compose.yml` file in the root of your project.\n2. Turn each of the `docker run` commands into a `service` in the `docker-compose.yml` file.\n3. This is re-creating the flags passed to the `docker run` command, but in YAML format.\n\nExample of the two `docker run`s above:\n\n```yml\nservices:\n  app:\n    image: node:12-alpine\n    command: sh -c \"npm install && npm run dev\"\n    ports:\n      - 3000:3000\n    working_dir: /app\n    volumes:\n      - ./:/app\n    environment:\n      MYSQL_HOST: mysql\n      MYSQL_USER: root\n      MYSQL_PASSWORD_FILE: /run/secrets/mysql_password\n      MYSQL_DB: todos\n  mysql:\n    image: mysql:5.7\n    platform: linux/amd64\n    volumes:\n      - todo-mysql-data:/var/lib/mysql\n    environment:\n      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password\n      MYSQL_DATABASE: todos\n\nvolumes:\n  todo-mysql-data:\n```\n\nThen, just run `docker compose up -d` and it will start in the background!\n\nYou can see it in Docker desktop.\n\nTear it down and remove the containers (but not the volumes) with `docker compose down`.\n\n## Caching in Dockerfile layers\n\nEach layer (i.e. each line of text) in a Dockerfile uses caching.\n\nThat means that if Docker doesn't detect that a layer has changed, it won't re-run it. It'll use the last value / files that were generated for the last build.\n\nHowever, it also means that if one layer changes and has to be re-built, Docker will re-build all subsequent layers.\n\nTherefore it's best to set up your Dockerfile so that you can maximise the amount of caching and reduce the chances of a cache bust.\n\nFor example, instead of this:\n\n```\nFROM node:12-alpine\nWORKDIR /app\nCOPY . .\nRUN npm install --production\nCMD [\"node\", \"src/index.js\"]\n```\n\nYou might do this:\n\n```\nFROM node:12-alpine\nWORKDIR /app\nCOPY package.json package.lock ./\nRUN npm install --production\nCOPY . .\nCMD [\"node\", \"src/index.js\"]\n```\n\nThat way if the `package.json` and `package.lock` files don't change, you won't re-run `npm install`.\n\nIn the first example, if _any_ code files changed, `npm install` would run. Even if it was not needed because the requirements file didn't change.\n\n### Ignore certain files and folders with `.dockerignore`\n\nSome files and folders can be safely ignored when copying over to the Docker container. For example, `node_modules` or the Python virtual environment.\n\nCreate a `.dockerignore` file in the root directory of your project (where `docker-compose.yml` lives), and add this (more examples of what to add for a Python project [here](https://github.com/GoogleCloudPlatform/getting-started-python/blob/main/optional-kubernetes-engine/.dockerignore)):\n\n```\nnode_modules\n.venv\n.env\n*.pyc\n__pycache__\n```\n\n:::danger Secrets in Docker images\nDon't include any secrets (like database connection strings or API keys) in your code. For local development you can use a `.env` file, but don't include the `.env` file in your Docker image!\n\nOne of the benefits of Docker images is you can share them with others easily, but that's why you have to be very careful with what you include in them.\n:::\n"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/README.md",
    "content": "# Run the REST API using Docker Compose\n\nNow that we've got a Docker container for our REST API, we can set up Docker Compose to run the container.\n\nDocker Compose is most useful to start multiple Docker containers at the same time, specifying configuration values for them and dependencies between them.\n\nLater 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.\n\nIf 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/).\n\n## How to write a `docker-compose.yml` file\n\nCreate a file called `docker-compose.yml` in the root of your project (alongside your `Dockerfile`). Inside it, add the following contents:\n\n```yaml\nservices:\n  web:\n    build: .\n    ports:\n      - \"5000:5000\"\n    volumes:\n      - .:/app\n```\n\nThis 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`).\n\nOther settings provided are:\n\n- `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.\n- `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.\n\n## How to run the Docker Compose services\n\nSimply type:\n\n```\ndocker compose up\n```\n\nAnd 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.\n\nWhen 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.\n\nIn 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.\n\nCongratulations, you've ran your first Docker Compose service!\n\n## Rebuilding the Docker image\n\nIf you need to rebuild the Docker image of your REST API service for whatever reason (e.g. configuration changes), you can run:\n\n```\ndocker compose up --build --force-recreate --no-deps web\n```\n\nMore information [here](https://stackoverflow.com/a/50802581).\n"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/end/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/end/docker-compose.yml",
    "content": "services:\n  web:\n    build: .\n    ports:\n      - \"5000:5000\"\n    volumes:\n      - .:/app"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/04_docker_intro/04_run_with_docker_compose/start/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/04_docker_intro/05_run_commands_in_docker_containers/README.md",
    "content": "# How to run commands inside a Docker container\n\nIf you run your API using Docker Compose, with the `docker compose up` command, you may also want to be able to execute arbitrary shell commands in the container.\n\nFor example, later on in the course we will look at database migrations.\n\nTo execute a database migration, we need to run a specific command, `flask db mgirate`.\n\nIf we use Docker Compose, we'll need to run the command inside the running container, and not in a local terminal.\n\nYou can run any arbitrary command in a running container like so:\n\n```bash\ndocker compose exec web flask db migrate\n```\n\nThis command is split into 4 parts:\n\n- `docker compose`: uses the Docker Compose part of the Docker executable\n- `exec`: used to run a command in a specific Docker Compose service\n- `web`: which Docker Compose service to run the command in\n- `flask db migrate`: the command you want to run\n\nThat's all! Just remember while following the course, that if I run any commands in my local terminal and you are using Docker Compose, you should precede the commands with `docker compose exec web`.\n"
  },
  {
    "path": "docs/docs/04_docker_intro/README.md",
    "content": "# An Introduction to Docker\n\n:::caution Not a Docker course\nAn important foreword: this is not a Docker course, and I'm not a Docker expert!\n\nIn this section, and in later sections of this course, I'll teach you what Docker is and how to use it to run and deploy your Flask apps. However, I won't teach you everything there is to know about Docker!\n:::\n\nDocker is a software framework for building, running, and managing **images** and **containers**.\n\nIn order to understand Docker, you need to clarify two questions:\n\n- What are Docker containers, and how are they different to Virtual Machines?\n- What are Docker images?\n\nAfter this, you'll be ready to create your own Docker images and use those images to create and run containers.\n\nLet's take a look at Docker containers in the next lecture!"
  },
  {
    "path": "docs/docs/04_docker_intro/_category_.json",
    "content": "{\n    \"label\": \"Introduction to Docker\",\n    \"position\": 4\n}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/01_why_flask_smorest/README.md",
    "content": "---\nctslug: why-use-flask-smorest\n---\n\n# Why use Flask-Smorest\n\nThere are many different REST API libraries for Flask. In a previous version of this course, we used Flask-RESTful. Now, I recommend using [Flask-Smorest](https://github.com/marshmallow-code/flask-smorest).\n\nOver the last few months, I've been trialing the major REST libraries for Flask. I've built REST APIs using Flask-RESTful, Flask-RESTX, and Flask-Smorest.\n\nI was looking to compare the three libraries in a few key areas:\n\n- **Ease of use and getting started**. Many REST APIs are essentially microservices, so being able to whip one up quickly and without having to go through a steep learning curve is definitely interesting.\n- **Maintainability and expandability**. Although many start as microservices, sometimes we have to maintain projects for a long time. And sometimes, they grow past what we originally envisioned.\n- **Activity in the library itself**. Even if a library is suitable now, if it is not actively maintained and improved, it may not be suitable in the future. We'd like to teach something that you will use for years to come.\n- **Documentation and usage of best practice**. The library should help you write better code by having strong documentation and guiding you into following best practice. If possible, it should use existing, actively maintained libraries as dependencies instead of implementing their own versions of them.\n- **Developer experience in production projects**. The main point here was: how easy is it to produce API documentation with the library of choice. Hundreds of students have asked me how to integrate Swagger in their APIs, so it would be great if the library we teach gave it to you out of the box.\n\n## Flask-Smorest is the most well-rounded\n\nIt ticks all the boxes above:\n\n- If you want, it can be super similar to Flask-RESTful (which is a compliment, really easy to get started!).\n- It uses [marshmallow](https://marshmallow.readthedocs.io/en/stable/) for serialization and deserialization, which is a huge plus. Marshmallow is a very actively-maintained library which is very intuitive and unlocks very easy argument validation. Unfortunately Flask-RESTX [doesn't use marshmallow](https://flask-restx.readthedocs.io/en/latest/marshalling.html), though there are [plans to do so](https://github.com/python-restx/flask-restx/issues/59).\n- It provides Swagger (with Swagger UI) and other documentations out of the box. It uses the same marshmallow schemas you use for API validation and some simple decorators in your code to generate the documentation.\n- The documentation is the weakest point (compared to Flask-RESTX), but with this course we can help you navigate it. The documentation of marshmallow is superb, so that will also help.\n\n## If you took an old version of this course...\n\nLet me tell you about some of the key differences between a project that uses Flask-RESTful and one that uses Flask-Smorest. After reading through these differences, it should be fairly straightforward for you to look at two projects, each using one library, and compare them.\n\n1. Flask-Smorest uses `flask.views.MethodView` classes registered under a `flask_smorest.Blueprint` instead of `flask_restful.Resource` classes.\n2. Flask-Smorest uses `flask_smorest.abort` to return error responses instead of manually returning the error JSON and error code.\n3. Flask-Smorest projects define marshmallow schemas that represent incoming data (for deserialization and validation) and outgoing data (for serialization). It uses these schemas to automatically validate the data and turn Python objects into JSON.\n\nThroughout this section I'll show you how to implement these 3 points in practice, so if you've already got a REST API that uses Flask-RESTful, you'll find it really easy to migrate.\n\nOf course, you can keep using Flask-RESTful for your existing projects, and only use Flask-Smorest for new projects. That's also an option! Flask-RESTful isn't abandoned or deprecated, so it's still a totally viable option."
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/README.md",
    "content": "---\ntitle: \"Data model improvements\"\ndescription: \"Use dictionaries instead of lists for data storage, and store stores and items separately.\"\nctslug: data-model-improvements\n---\n\n# Data model improvements\n\n## Starting code from section 4\n\nThis is the \"First REST API\" project from Section 4:\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"app\" label=\"app.py\" default>\n\n```py title=\"app.py\"\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [\n    {\n        \"name\": \"My Store\",\n        \"items\": [\n            {\n                \"name\": \"Chair\",\n                \"price\": 15.99\n            }\n        ]\n    }\n]\n\n@app.get(\"/store\")  # http://127.0.0.1:5000/store\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n```\n\n</TabItem>\n<TabItem value=\"docker\" label=\"Dockerfile\">\n\n```docker\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\n</TabItem>\n</Tabs>\n</div>\n\n## New files\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n\nThere are two Insomnia files for this section: one for lectures 1-5 (before adding Docker), and one for the other lectures (after adding Docker).\n:::\n\nLet's start off by creating a `requirements.txt` file with all our dependencies:\n\n```txt title=\"requirements.txt\"\nflask\nflask-smorest\npython-dotenv\n```\n\nWe're adding `flask-smorest` to help us write REST APIs more easily, and generate documentation for us.\n\nWe're adding `python-dotenv` so it's easier for us to load environment variables and use the `.flaskenv` file.\n\nNext, let's create the `.flaskenv` file:\n\n```txt title=\".flaskenv\"\nFLASK_APP=app\nFLASK_DEBUG=True\n```\n\nIf we have the `python-dotenv` library installed, when we run the `flask run` command, Flask will read the variables inside `.flaskenv` and use them to configure the Flask app.\n\nThe configuration that we'll do is to define the Flask app file (here, `app.py`). Then we'll also set the `FLASK_DEBUG` flag to `True`, which does a couple things:\n\n- Makes the app give us better error messages and return a traceback when we make requests if there's an error.\n- Sets the app reloading to true, so the app restarts when we make code changes\n\nWe don't want debug mode to be enabled in production (when we deploy our app), but while we're doing development it's definitely a time-saving tool!\n\n## Code improvements\n\n### Creating a database file\n\nFirst of all, let's move our \"database\" to another file.\n\nCreate a `db.py` file with the following content:\n\n```py title=\"db.py\"\nstores = {}\nitems = {}\n```\n\nIn the existing code we only have a `stores` list, so delete that from `app.py`. From now on we will be storing information about items and stores separately.\n\n:::tip What is in each dictionary?\nEach dictionary will closely mimic how a database works: a mapping of ID to data. So each dictionary will be something like this:\n\n```py\n{\n    1: {\n        \"name\": \"Chair\",\n        \"price\": 17.99\n    },\n    2: {\n        \"name\": \"Table\",\n        \"price\": 180.50\n    }\n}\n```\n\nThis will make it much easier to retrieve a specific store or item, just by knowing its ID.\n:::\n\nThen, import the `stores` and `items` variables from `db.py` in `app.py`:\n\n```py title=\"app.py\"\nfrom db import stores, items\n```\n\n## Using stores and items in our API\n\nNow let's make use of stores and items separately in our API.\n\n### `get_store`\n\nHere are the changes we'll need to make:\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"old\" label=\"get_store (old)\" default>\n\n```py title=\"app.py\"\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n```\n\n</TabItem>\n<TabItem value=\"new\" label=\"get_store (new)\">\n\n```py title=\"app.py\"\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        return {\"message\": \"Store not found\"}, 404\n```\n\nImportant to note that in this version, we won't return the items in the store. That's a limitation of our dictionaries-for-database setup that we will solve when we introduce databases!\n\n</TabItem>\n</Tabs>\n</div>\n\n### `get_stores`\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"old\" label=\"get_stores (old)\" default>\n\n```py title=\"app.py\"\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n```\n\n</TabItem>\n<TabItem value=\"new\" label=\"get_stores (new)\">\n\n```py title=\"app.py\"\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n```\n\n</TabItem>\n</Tabs>\n</div>\n\n### `create_store`\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"old\" label=\"create_store (old)\" default>\n\n```py title=\"app.py\"\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n```\n\n</TabItem>\n<TabItem value=\"new\" label=\"create_store (new)\">\n\n```py title=\"app.py\"\nimport uuid\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n```\n\nHere we add a new import, [the `uuid` module](https://docs.python.org/3/library/uuid.html). We will be using it to create unique IDs for our stores and items instead of relying on the uniqueness of their names.\n\n</TabItem>\n</Tabs>\n</div>\n\n### `create_item`\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"old\" label=\"create_item (old)\" default>\n\n```py title=\"app.py\"\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n```\n\n</TabItem>\n<TabItem value=\"new\" label=\"create_item (new)\">\n\n```py title=\"app.py\"\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    if item_data[\"store_id\"] not in stores:\n        return {\"message\": \"Store not found\"}, 404\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n```\n\nNow we are POSTing to `/item` instead of `/store/<string:name>/item`. The endpoint will expect to receive JSON with `price`, `name`, and `store_id`.\n\n</TabItem>\n</Tabs>\n</div>\n\n\n### `get_items` (new)\n\nThis is not an endpoint we could easily make when we were working with a single `stores` list!\n\n```py\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n```\n\n### `get_item_in_store`\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"old\" label=\"get_item_in_store (old)\" default>\n\n```py title=\"app.py\"\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n```\n\n</TabItem>\n<TabItem value=\"new\" label=\"get_item (new)\">\n\n```py title=\"app.py\"\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        return {\"message\": \"Item not found\"}, 404\n```\n\nNow we are GETting from `/item` instead of `/store/<string:name>/item`. This is because while items are related to stores, they aren't inside a store anymore!\n\n</TabItem>\n</Tabs>\n</div>\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/end/Dockerfile",
    "content": "# In the course we run the app outside Docker\n# until lecture 5.\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/end/app.py",
    "content": "import uuid\nfrom flask import Flask, request\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        return {\"message\": \"Item not found\"}, 404\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    if item_data[\"store_id\"] not in stores:\n        return {\"message\": \"Store not found\"}, 404\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        return {\"message\": \"Store not found\"}, 404\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/02_data_model_improvements/start/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [{\"name\": \"My Store\", \"items\": [{\"name\": \"Chair\", \"price\": 15.99}]}]\n\n\n@app.get(\"/store\")  # http://127.0.0.1:5000/store\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/README.md",
    "content": "---\ntitle: \"Improvements to our first REST API\"\ndescription: \"Add new error handling and code improvements to the REST API before adding any new endpoints.\"\nctslug: improvements-to-our-first-rest-api\n---\n\n# Improvements to our first REST API\n\n## Using `flask_smorest.abort` instead of returning errors manually\n\nAt the moment in our API we're doing things like these in case of an error:\n\n```py title=\"app.py\"\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        return {\"message\": \"Store not found\"}, 404\n```\n\nA small improvement we can do on this is use the `abort` function from Flask-Smorest, which helps us write these messages and include a bit of extra information too.\n\nAdd this import at the top of `app.py`:\n\n```py title=\"app.py\"\nfrom flask_smorest import abort\n```\n\nAnd then let's change our error returns to use `abort`.\n\n```py title=\"app.py\"\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        # highlight-start\n        abort(404, message=\"Store not found.\")\n        # highlight-end\n```\n\nAnd here:\n\n```py title=\"app.py\"\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        # highlight-start\n        abort(404, message=\"Item not found.\")\n        # highlight-end\n```\n\n## Adding error handling on creating items and stores\n\nAt the moment when we create items and stores, we _expect_ there to be certain items in the JSON body of the request.\n\nIf those items are missing, the app will return an error 500, which means \"Internal Server Error\".\n\nInstead of that, it's good practice to return an error 400 and a message telling the client what went wrong.\n\nTo do so, let's inspect the body of the request and see if it contains the data we need.\n\nLet's change our `create_item()` function to this:\n\n```py title=\"app.py\"\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n```\n\nAnd our `create_store()` function to this:\n\n```py title=\"app.py\"\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n```"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/app.py",
    "content": "import uuid\nfrom flask import Flask, request\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        return {\"message\": \"Item not found\"}, 404\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    if item_data[\"store_id\"] not in stores:\n        return {\"message\": \"Store not found\"}, 404\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        return {\"message\": \"Store not found\"}, 404\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/README.md",
    "content": "---\ntitle: \"New endpoints for our REST API\"\ndescription: \"Let's add a few routes to our first REST API, so it better matches what a production REST API would look like.\"\nctslug: new-endpoints-for-our-rest-api\n---\n\n# New endpoints for our REST API\n\n## New endpoints\n\nWe want to add some endpoints for added functionality:\n\n- `DELETE /item/<string:item_id>` so we can delete items from the database.\n- `PUT /item/<string:item_id>` so we can update items.\n- `DELETE /store/<string:store_id>` so we can delete stores.\n\n### Deleting items\n\nThis is almost identical to getting items, but we use the `del` keyword to remove the entry from the dictionary.\n\n```py title=\"app.py\"\n@app.delete(\"/item/<string:item_id>\")\ndef delete_item(item_id):\n    try:\n        del items[item_id]\n        return {\"message\": \"Item deleted.\"}\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\n### Updating items\n\nThis is almost identical to creating items, but in this API we've decided to not let item updates change the `store_id` of the item. So clients can change item name and price, but not the store that the item belongs to.\n\nThis is an API design decision, and you could very well allow clients to update the `store_id` if you want!\n\n```py title=\"app.py\"\n@app.put(\"/item/<string:item_id>\")\ndef update_item(item_id):\n    item_data = request.get_json()\n    # There's  more validation to do here!\n    # Like making sure price is a number, and also both items are optional\n    # You should also prevent keys that aren't 'price' or 'name' to be passed\n    # Difficult to do with an if statement...\n    if \"price\" not in item_data or \"name\" not in item_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n        )\n    try:\n        item = items[item_id]\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\n:::tip Dictionary update operators\nThe `|=` syntax is a new dictionary operator. You can read more about it [here](https://blog.teclado.com/python-dictionary-merge-update-operators/).\n:::\n\n### Deleting stores\n\nThis is very similar to deleting items!\n\n```py title=\"app.py\"\n@app.delete(\"/store/<string:store_id>\")\ndef delete_store(store_id):\n    try:\n        del stores[store_id]\n        return {\"message\": \"Store deleted.\"}\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n```\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.delete(\"/item/<string:item_id>\")\ndef delete_item(item_id):\n    try:\n        del items[item_id]\n        return {\"message\": \"Item deleted.\"}\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.put(\"/item/<string:item_id>\")\ndef update_item(item_id):\n    item_data = request.get_json()\n    # There's  more validation to do here!\n    # Like making sure price is a number, and also both items are optional\n    # Difficult to do with an if statement...\n    if \"price\" not in item_data or \"name\" not in item_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n        )\n    try:\n        item = items[item_id]\n\n        # https://blog.teclado.com/python-dictionary-merge-update-operators/\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.delete(\"/store/<string:store_id>\")\ndef delete_store(store_id):\n    try:\n        del stores[store_id]\n        return {\"message\": \"Store deleted.\"}\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/README.md",
    "content": "---\ntitle: \"Reloading API code in Docker container\"\ndescription: \"Learn how to get your code instantly synced up to the Docker container, so that every time you make a code change it restarts the app in the container and uses the latest code.\"\nctslug: reloading-api-code-in-docker-container\n---\n\n# Reloading API code in Docker container\n\n## Updating Dockerfile to use `requirements.txt`\n\nThis is the Dockerfile as we've got it:\n\n```dockerfile\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\nBut there is a problem! It doesn't use the `requirements.txt`, so it only installs Flask as a dependency.\n\nWe want to add `requirements.txt` and install the dependencies from it. You might be tempted to move the `COPY` line above the `RUN` line, and then install it with `pip install -r requirements.txt`.\n\nBut there's a better way!\n\n```dockerfile\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\nHere we:\n\n- Add a new `COPY` line that copies the `requirements.txt` file into the image. This creates a new cached layer, so that if the `requirements.txt` file doesn't change, this line and the following `RUN` line don't run again.\n- Change the `pip install` code to use `--no-cache-dir --upgrade`. This makes sure that we don't use any pre-existing pip caches when installing, and also upgrades libraries to the latest version if necessary.\n\n## Running the container with volumes for hot reloading\n\nUp to now, we've been re-building the Docker image and re-running the container each time we make a code change.\n\nThis is a bit of a time sink, and a bit annoying to do! Let's do it so that the Docker container runs the code that we're editing. That way, when we make a change to the code, the Flask app should restart and use the new code.\n\nAll we have to do is:\n\n1. Build the Docker image\n2. Run the image, but replace the contents of the image's `/app` directory (where the code is) by the contents of our source code folder in the host machine.\n\nSo, first build the Docker image:\n\n```\ndocker build -t flask-smorest-api .\n```\n\nOnce that's done, the image has an `/app` directory which contains the source code as it was copied from the host machine during the build stage.\n\nSo at this point, we _can_ run a container from this image, and it will run the app _as it was when it was built_:\n\n```\ndocker run -dp 5000:5000 flask-smorest-api\n```\n\nThis should just work, and you can try it out in the Insomnia REST Client to make sure the endpoints all work.\n\nBut like we said earlier, when we make changes to the code we'll have to rebuild and rerun.\n\nSo instead, what we can do is run the image, but replace the image's `/app` directory with the host's source code folder.\n\nThat will cause the source code to change in the Docker container while it's running. And, since we've ran Flask with debug mode on, the Flask app will automatically restart when the code changes.\n\nTo do so, stop the running container (if you have one running), and use this command instead:\n\n```\ndocker run -dp 5000:5000 -w /app -v \"$(pwd):/app\" flask-smorest-api\n```\n\n:::info Windows command\nThe command on Windows varies depending on what terminal application you use. Here are some of the most popular ones!\n\n**PowerShell**\n\n```\ndocker run -dp 5000:5000 -w //app -v \"$(Get-Location)://app\" flask-smorest-api\n```\n\n**Git Bash**\n\n```\ndocker run -dp 5000:5000 -w //app -v \"//$(pwd)://app\" flask-smorest-api\n```\n\n**Command Prompt (CMD)**\n\n```\ndocker run -dp 5000:5000 -w //app -v \"%cd%://app\" flask-smorest-api\n```\n:::\n\n- `-dp 5000:5000` - same as before. Run in detached (background) mode and create a port mapping.\n- `-w /app` - sets the container's present working directory where the command will run from.\n- `-v \"$(pwd):/app\"` - bind mount (link) the host's present directory to the container's `/app` directory. Note: Docker requires absolute paths for binding mounts, so in this example we use `pwd` for printing the absolute path of the working directory instead of typing it manually.\n- `flask-smorest-api` - the image to use.\n\nAnd with this, your Docker container now is running the code as shown in your IDE. Plus, since Flask is running with debug mode on, the Flask app will restart when you make code changes!\n\n:::info\nUsing this kind of volume mapping only makes sense _during development_. When you share your Docker image or deploy it, you won't be sharing anything from the host to the container. That's why it's still important to include the original source code in the image when you build it.\n:::\n\nJust to recap, here are the two ways we've seen to run your Docker container:\n\n![Diagram showing two ways of running a Docker container from a built image, with and without volume mapping](https://res.cloudinary.com/teclado/image/upload/v1689180724/courses/rest-apis-flask-python/build-with-without-volume_a7mig8.png)"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/end/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.delete(\"/item/<string:item_id>\")\ndef delete_item(item_id):\n    try:\n        del items[item_id]\n        return {\"message\": \"Item deleted.\"}\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.put(\"/item/<string:item_id>\")\ndef update_item(item_id):\n    item_data = request.get_json()\n    # There's  more validation to do here!\n    # Like making sure price is a number, and also both items are optional\n    # Difficult to do with an if statement...\n    if \"price\" not in item_data or \"name\" not in item_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n        )\n    try:\n        item = items[item_id]\n\n        # https://blog.teclado.com/python-dictionary-merge-update-operators/\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.delete(\"/store/<string:store_id>\")\ndef delete_store(store_id):\n    try:\n        del stores[store_id]\n        return {\"message\": \"Store deleted.\"}\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/start/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:item_id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.delete(\"/item/<string:item_id>\")\ndef delete_item(item_id):\n    try:\n        del items[item_id]\n        return {\"message\": \"Item deleted.\"}\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.put(\"/item/<string:item_id>\")\ndef update_item(item_id):\n    item_data = request.get_json()\n    # There's  more validation to do here!\n    # Like making sure price is a number, and also both items are optional\n    # Difficult to do with an if statement...\n    if \"price\" not in item_data or \"name\" not in item_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n        )\n    try:\n        item = items[item_id]\n\n        # https://blog.teclado.com/python-dictionary-merge-update-operators/\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.get(\"/store/<string:store_id>\")\ndef get_store(store_id):\n    try:\n        # Here you might also want to add the items in this store\n        # We'll do that later on in the course\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n\n\n@app.delete(\"/store/<string:store_id>\")\ndef delete_store(store_id):\n    try:\n        del stores[store_id]\n        return {\"message\": \"Store deleted.\"}\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/05_reload_api_docker_container/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/README.md",
    "content": "---\ntitle: How to use Blueprints and MethodViews\ndescription: Flask-Smorest MethodViews allow us to simplify API Resources by defining all methods that interact with the resource in one Python class.\nctslug: how-to-use-flask-smorest-methodviews-blueprints\n---\n\n# How to use Flask-Smorest MethodViews and Blueprints\n\nLet's improve the structure of our code by splitting items and stores endpoints into their own files.\n\nLet's create a `resources` folder, and inside it create `item.py` and `store.py`.\n\n## Creating a blueprint for each related group of resources\n\n### `resources/store.py`\n\nLet's start in `store.py`, and create a `Blueprint`:\n\n```py title=\"resources/store.py\"\nimport uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n```\n\nThe `Blueprint` arguments are the same as the Flask `Blueprint`[^1], with an added optional `description` keyword argument:\n\n1. `\"stores\"` is the name of the blueprint. This will be shown in the documentation and is prepended to the endpoint names when you use `url_for` (we won't use it).\n2. `__name__` is the \"import name\".\n3. The `description` will be shown in the documentation UI.\n\n\nNow that we've got this, let's add our `MethodView`s. These are classes where each method maps to one endpoint. The interesting thing is that method names are important:\n\n```py title=\"resources/store.py\"\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(self, store_id):\n        pass\n\n    def delete(self, store_id):\n        pass\n```\n\nTwo things are going on here:\n\n1. The endpoint is associated to the `MethodView` class. Here, the class is called `Store` and the endpoint is `/store/<string:store_id>`.\n2. There are two methods inside the `Store` class: `get` and `delete`. These are going to map directly to `GET /store/<string:store_id>` and `DELETE /store/<string:store_id>`.\n\nNow we can copy the code from earlier into each of the methods:\n\n```py title=\"resources/store.py\"\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(self, store_id):\n        try:\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(self, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n```\n\nNow, still inside the same file, we can add another `MethodView` with a different endpoint, for the `/store` route:\n\n```py title=\"resources/store.py\"\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(self):\n        return {\"stores\": list(stores.values())}\n\n    def post(self):\n        store_data = request.get_json()\n        if \"name\" not in store_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n            )\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n```\n\n### `resources/item.py`\n\nLet's do the same thing with the `resources/item.py` file:\n\n```py title=\"resources/item.py\"\nimport uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def put(self, item_id):\n        item_data = request.get_json()\n        # There's  more validation to do here!\n        # Like making sure price is a number, and also both items are optional\n        # Difficult to do with an if statement...\n        if \"price\" not in item_data or \"name\" not in item_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n            )\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    def post(self):\n        item_data = request.get_json()\n        # Here not only we need to validate data exists,\n        # But also what type of data. Price should be a float,\n        # for example.\n        if (\n            \"price\" not in item_data\n            or \"store_id\" not in item_data\n            or \"name\" not in item_data\n        ):\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n            )\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n```\n\n## Import blueprints and Flask-Smorest configuration\n\nFinally, we have to import the `Blueprints` inside `app.py`, and register them with Flask-Smorest:\n\n```py title=\"app.py\"\nfrom flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n```\n\nI've also added a few config variables to the `app.config`. The `PROPAGATE_EXCEPTIONS` value is used so that when an exception is raised in an extension, it is bubbled up to the main Flask app so you'd see it more easily.\n\nThe other config values are there for the documentation of our API, and they define things such as the API name and version, as well as information for the Swagger UI.\n\nNow you should be able to go to `http://127.0.0.1:5000/swagger-ui` and see your Swagger documentation rendered out!\n\n[^1]: [Flask Blueprint (Flask Official Documentation)](https://flask.palletsprojects.com/en/2.1.x/api/#flask.Blueprint)"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/item.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def put(self, item_id):\n        item_data = request.get_json()\n        # There's  more validation to do here!\n        # Like making sure price is a number, and also both items are optional\n        # Difficult to do with an if statement...\n        if \"price\" not in item_data or \"name\" not in item_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n            )\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    def post(self):\n        item_data = request.get_json()\n        # Here not only we need to validate data exists,\n        # But also what type of data. Price should be a float,\n        # for example.\n        if (\n            \"price\" not in item_data\n            or \"store_id\" not in item_data\n            or \"name\" not in item_data\n        ):\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n            )\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(self, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(self, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(self):\n        return {\"stores\": list(stores.values())}\n\n    def post(self):\n        store_data = request.get_json()\n        if \"name\" not in store_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n            )\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/start/app.py",
    "content": "import uuid\nfrom flask import Flask, request\nfrom flask_smorest import abort\n\nfrom db import stores, items\n\n\napp = Flask(__name__)\n\n\n@app.get(\"/item/<string:id>\")\ndef get_item(item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.delete(\"/item/<string:id>\")\ndef delete_item(item_id):\n    try:\n        del items[item_id]\n        return {\"message\": \"Item deleted.\"}\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.put(\"/item/<string:id>\")\ndef update_item(item_id):\n    item_data = request.get_json()\n    # There's  more validation to do here!\n    # Like making sure price is a number, and also both items are optional\n    # Difficult to do with an if statement...\n    if \"price\" not in item_data or \"name\" not in item_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n        )\n    try:\n        item = items[item_id]\n\n        # https://blog.teclado.com/python-dictionary-merge-update-operators/\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n\n\n@app.get(\"/item\")\ndef get_all_items():\n    return {\"items\": list(items.values())}\n\n\n@app.post(\"/item\")\ndef create_item():\n    item_data = request.get_json()\n    # Here not only we need to validate data exists,\n    # But also what type of data. Price should be a float,\n    # for example.\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n\n\n@app.get(\"/store/<string:id>\")\ndef get_store(store_id):\n    try:\n        # You presumably would want to include the store's items here too\n        # More on that when we look at databases\n        return stores[store_id]\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.delete(\"/store/<string:id>\")\ndef delete_store(store_id):\n    try:\n        del stores[store_id]\n        return {\"message\": \"Store deleted.\"}\n    except KeyError:\n        abort(404, message=\"Store not found.\")\n\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": list(stores.values())}\n\n\n@app.post(\"/store\")\ndef create_store():\n    store_data = request.get_json()\n    if \"name\" not in store_data:\n        abort(\n            400,\n            message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n        )\n    for store in stores.values():\n        if store_data[\"name\"] == store[\"name\"]:\n            abort(400, message=f\"Store already exists.\")\n\n    store_id = uuid.uuid4().hex\n    store = {**store_data, \"id\": store_id}\n    stores[store_id] = store\n\n    return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/06_api_with_method_views/start/requirements.txt",
    "content": "flask\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/README.md",
    "content": "---\ntitle: Adding marshmallow schemas\ndescription: A marshmallow schema is useful for validation and serialization. Learn how to write them in this lecture.\nctslug: adding-marshmallow-schemas\n---\n\n# Adding marshmallow schemas\n\nSomething that we're lacking in our API at the moment is validation. We've done a _tiny_ bit of it with this kind of code:\n\n```py\nif (\n    \"price\" not in item_data\n    or \"store_id\" not in item_data\n    or \"name\" not in item_data\n):\n    abort(\n        400,\n        message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n    )\n```\n\nBut there's so much more we can do. For starters, some data points may be optional in some endpoints. We also want to check the data type is correct (i.e. `price` shouldn't be a string, for example).\n\nTo do this kind of checking we can construct a massive `if` statement, or we can use a library that is made specifically for it.\n\nThe `marshmallow`[^1] library is used to define _what_ data fields we want, and then we can pass incoming data through the validator. We can also go the other way round, and give it a Python object which `marshmallow` then turns into a dictionary.\n\n## Writing the `ItemSchema`\n\nHere's the definition of an `Item` using `marshmallow` (this is called a **schema**):\n\n```py title=\"schemas.py\"\nfrom marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n```\n\nA couple of weird things maybe!\n\nThe `id` field is a string, but it has the `dump_only=True` argument. This means that when we use marshmallow to _validate incoming data_, the `id` field won't be used or expected. However, when we use marshmallow to _serialize_ data to be returned to a client, the `id` field will be included in the output.\n\nThe other fields will be used for both validation and serialization, and since they have the `required=True` argument, that means that when we do validation if the fields are not present, an error will be raised.\n\n`marshmallow` will also check the data type with `fields.Float` and `fields.Int`.\n\n## Writing the `ItemUpdateSchema`\n\nSomething that even to do this day sits a bit weird with me is having multiple different schemas for different applications.\n\nWhen we want to update an Item, we have different requirements than when we want to create an item.\n\nThe main difference is that the incoming data to our API when we update an item is different than when we create one. Fields are optional, such that not all item fields should be required. Also, you may not want to allow certain fields _at all_.\n\nThis is the `ItemUpdateSchema`:\n\n```py title=\"schemas.py\"\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n```\n\nAs you can see, these are not `required=True`. I've also taken off the `id` and `store_id` fields, because:\n\n- This schema will only be used for incoming data, and we will never receive an `id`.\n- We don't want clients to be able to change the `store_id` of an item. If you wanted to allow this, you can add the `store_id` field here as well.\n\n## Writing the `StoreSchema`\n\n```py title=\"schemas.py\"\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n```\n\nThere's not much to explain here! Similar to the `ItemSchema`, we have `id` and `name` since those are the only fields we need for a store."
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/item.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def put(self, item_id):\n        item_data = request.get_json()\n        # There's  more validation to do here!\n        # Like making sure price is a number, and also both items are optional\n        # Difficult to do with an if statement...\n        if \"price\" not in item_data or \"name\" not in item_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n            )\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    def post(self):\n        item_data = request.get_json()\n        # Here not only we need to validate data exists,\n        # But also what type of data. Price should be a float,\n        # for example.\n        if (\n            \"price\" not in item_data\n            or \"store_id\" not in item_data\n            or \"name\" not in item_data\n        ):\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n            )\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(cls):\n        return {\"stores\": list(stores.values())}\n\n    def post(cls):\n        store_data = request.get_json()\n        if \"name\" not in store_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n            )\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/item.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def put(self, item_id):\n        item_data = request.get_json()\n        # There's  more validation to do here!\n        # Like making sure price is a number, and also both items are optional\n        # Difficult to do with an if statement...\n        if \"price\" not in item_data or \"name\" not in item_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n            )\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    def post(self):\n        item_data = request.get_json()\n        # Here not only we need to validate data exists,\n        # But also what type of data. Price should be a float,\n        # for example.\n        if (\n            \"price\" not in item_data\n            or \"store_id\" not in item_data\n            or \"name\" not in item_data\n        ):\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n            )\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(cls):\n        return {\"stores\": list(stores.values())}\n\n    def post(cls):\n        store_data = request.get_json()\n        if \"name\" not in store_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n            )\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/README.md",
    "content": "---\ntitle: Validation with marshmallow\ndescription: We can use the marshmallow library to validate request data from our API clients.\nctslug: validation-with-marshmallow\n---\n\n# Validation with marshmallow\n\nNow that we've got our schemas written, let's use them to validate incoming data to our API.\n\nWith Flask-Smorest, this couldn't be easier!\n\nLet's start with `resources/item.py`\n\n## Validation in `resources/item.py`\n\nAt the top of the file, import the schemas:\n\n```py\nfrom schemas import ItemSchema, ItemUpdateSchema\n```\n\nWe have two sets of data that may be incoming (in the JSON body of a request): new items and updating items.\n\nSo let's go to the `ItemList#post` method and make a couple changes!\n\nFirst, let's get rid of the existing data validation. Delete the highlighted lines below:\n\n```py\ndef post(self):\n    # highlight-start\n    item_data = request.get_json()\n    if (\n        \"price\" not in item_data\n        or \"store_id\" not in item_data\n        or \"name\" not in item_data\n    ):\n        abort(\n            400,\n            message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n        )\n    # highlight-end\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n```\n\nNow, I know what you're thinking! What about `item_data`? Do we not need to keep that?\n\nWhen we use `marshmallow` for validation with Flask-Smorest, it will inject the validated data into our method for us.\n\nLook at these two highlighted lines:\n\n```py\n# highlight-start\n@blp.arguments(ItemSchema)\ndef post(self, item_data):\n    # highlight-end\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n```\n\nNice!\n\nPlus, doing this also adds to your Swagger UI documentation.\n\nLet's do the same when updating items:\n\n```py\n# highlight-start\n@blp.arguments(ItemUpdateSchema)\ndef put(self, item_data, item_id):\n    # highlight-end\n    try:\n        item = items[item_id]\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\n:::caution Order of parameters\nBe careful here since we've now got `item_data` and `item_id`. The URL arguments come in at the end. The injected arguments are passed first, so `item_data` goes before `item_id` in our function signature.\n:::\n\n## Validation in `resources/store.py`\n\nNow let's do the same in `store.py`!\n\nAt the top of the file, import the schema:\n\n```py\nfrom schemas import StoreSchema\n```\n\nWhen creating a store, we'll have this:\n\n```py\n@blp.arguments(StoreSchema)\ndef post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n```"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/item.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom schemas import ItemSchema, ItemUpdateSchema\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    def put(self, item_data, item_id):\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    @blp.arguments(ItemSchema)\n    def post(self, item_data):\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/store.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(cls):\n        return {\"stores\": list(stores.values())}\n\n    @blp.arguments(StoreSchema)\n    def post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/item.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def put(self, item_id):\n        item_data = request.get_json()\n        # There's  more validation to do here!\n        # Like making sure price is a number, and also both items are optional\n        # Difficult to do with an if statement...\n        if \"price\" not in item_data or \"name\" not in item_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', and 'name' are included in the JSON payload.\",\n            )\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    def post(self):\n        item_data = request.get_json()\n        # Here not only we need to validate data exists,\n        # But also what type of data. Price should be a float,\n        # for example.\n        if (\n            \"price\" not in item_data\n            or \"store_id\" not in item_data\n            or \"name\" not in item_data\n        ):\n            abort(\n                400,\n                message=\"Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.\",\n            )\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(cls):\n        return {\"stores\": list(stores.values())}\n\n    def post(cls):\n        store_data = request.get_json()\n        if \"name\" not in store_data:\n            abort(\n                400,\n                message=\"Bad request. Ensure 'name' is included in the JSON payload.\",\n            )\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/README.md",
    "content": "---\ntitle: Decorating responses with Flask-Smorest\ndescription: Add response serialization and status code to API endpoints, and add to your documentation in the process.\nctslug: decorating-responses-with-flask-smorest\n---\n\n# Decorating responses with Flask-Smorest\n\nWe can use marshmallow schemas for serialization when we respond to a client. To do so, we need to tell Flask-Smorest what Schema to use when responding.\n\nThis will do a few things:\n\n1. Update your documentation to show what data and status code will be returned by the endpoint.\n2. Pass any data your endpoint returns through the marshmallow schema, casting data types and removing data that isn't in the schema.\n\n## Decorating responses in `resources/item.py`\n\nLet's start with retrieving a specific item.\n\nUp until now, we've been doing this:\n\n```py\ndef get(self, item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\nBut now we can run the `items[item_id]` dictionary through the marshmallow schema and tell Flask-Smorest about it so the documentation will be updated:\n\n```py\n@blp.response(200, ItemSchema)\ndef get(self, item_id):\n    try:\n        return items[item_id]\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\n:::info\nThe number, `200`, is the status code. It means \"OK\" (all good).\n:::\n\nOur endpoint for updating items looks like this:\n\n```py\n@blp.arguments(ItemUpdateSchema)\ndef put(self, item_data, item_id):\n    try:\n        item = items[item_id]\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\nLet's pass this through the schema as well:\n\n```py\n@blp.arguments(ItemUpdateSchema)\n# highlight-start\n@blp.response(200, ItemSchema)\n# highlight-end\ndef put(self, item_data, item_id):\n    try:\n        item = items[item_id]\n\n        # https://blog.teclado.com/python-dictionary-merge-update-operators/\n        item |= item_data\n\n        return item\n    except KeyError:\n        abort(404, message=\"Item not found.\")\n```\n\n:::caution\nCareful with the order of decorators in these functions!\n:::\n\nWhen we get to returning a list of items, it looks like this:\n\n```py\n# highlight-start\n@blp.response(200, ItemSchema(many=True))\n# highlight-end\ndef get(self):\n    return items.values()\n```\n\nAnd finally, don't forget to decorate the new item endpoint too:\n\n```py\n@blp.arguments(ItemSchema)\n# highlight-start\n@blp.response(201, ItemSchema)\n# highlight-end\ndef post(self, item_data):\n    for item in items.values():\n        if (\n            item_data[\"name\"] == item[\"name\"]\n            and item_data[\"store_id\"] == item[\"store_id\"]\n        ):\n            abort(400, message=f\"Item already exists.\")\n\n    item_id = uuid.uuid4().hex\n    item = {**item_data, \"id\": item_id}\n    items[item_id] = item\n\n    return item\n```\n\n## Decorating responses in `resources/store.py`\n\nGoing a bit more quickly here since you already know what's going on with this decorator. The highlighted lines are new:\n\n```py title=\"resources/store.py\"\nimport uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    # highlight-start\n    @blp.response(200, StoreSchema)\n    # highlight-end\n    def get(cls, store_id):\n        try:\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    # highlight-start\n    @blp.response(200, StoreSchema(many=True))\n    # highlight-end\n    def get(cls):\n        return stores.values()\n\n    @blp.arguments(StoreSchema)\n    # highlight-start\n    @blp.response(201, StoreSchema)\n    # highlight-end\n    def post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n```"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/resources/item.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom schemas import ItemSchema, ItemUpdateSchema\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return items.values()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/resources/store.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(cls):\n        return stores.values()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/resources/item.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom schemas import ItemSchema, ItemUpdateSchema\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    def put(self, item_data, item_id):\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    def get(self):\n        return {\"items\": list(items.values())}\n\n    @blp.arguments(ItemSchema)\n    def post(self, item_data):\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/resources/store.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    def get(cls):\n        return {\"stores\": list(stores.values())}\n\n    @blp.arguments(StoreSchema)\n    def post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/09_decorating_responses/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/05_flask_smorest/Insomnia_section5_Docker.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:36:20.139Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_08302ba35f784bdc9fa2edc0cb080287\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985452213,\"created\":1666905719010,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719008,\"created\":1666905719008,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"parentId\":null,\"modified\":1666991880304,\"created\":1666905718998,\"name\":\"Section 5 - Docker\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_6fdedbe47a9941af9b8459816f179274\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985886605,\"created\":1666905719013,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0c240b23280746a6a1a56d7644fb89ce\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666987464108,\"created\":1666905719011,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fc255f6789fe45ed80b2ef83e6bb6645\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985462540,\"created\":1666905719014,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b42e3c4d855a433394ac1a8a60c2b91b\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666985467338,\"created\":1666905719020,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_80dee5df10c347198d8f12d85703d582\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719016,\"created\":1666905719016,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_9a89b2ecfc61457d8cac15985597c0a0\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666986841489,\"created\":1666905719023,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_3d189bf5d88349e3bce363a420407f65\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666987468265,\"created\":1666905719018,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": \\\"8efca659f8674c56b5cd035ecc0d42ec\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_35d865c76bce4e1b9c378d82ece413f7\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666985474126,\"created\":1666905719019,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_c2bf495d5cbb49d8b933b832a717662a\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666987071504,\"created\":1666905719022,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_adf22718b4e044e5b54b37c869463582\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666985430514,\"created\":1666905719000,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_210b7ba8709f44f29c305ed544da17c3\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719004,\"created\":1666905719004,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_7a427f233a494727845a45ba1325ea85\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719034,\"created\":1666905719007,\"fileName\":\"Flask-Smorest-Docker\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/05_flask_smorest/Insomnia_section5_before_Docker.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:35:47.649Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_3d2b5cd58a4b4a6983c133118c5f8027\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666125193227,\"created\":1666124761134,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_afac4dd2683746c586c6ff61228611de\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666125229064,\"created\":1666124761133,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"parentId\":null,\"modified\":1666991873213,\"created\":1666124761123,\"name\":\"Section 5 before Docker\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_bd3ecff11e5b49baa489812528235afb\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902781180,\"created\":1666124761139,\"url\":\"http://127.0.0.1:5000/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b9dafd45675e4c478fa4dd125f4827b3\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902941803,\"created\":1666124761136,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_34cbd59313d44bbfa4fd70166e341b05\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902749338,\"created\":1666124977832,\"url\":\"http://127.0.0.1:5000/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_637d0fb6ba9d4c25b6ad9f5bdda73036\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902961406,\"created\":1666125038450,\"url\":\"http://127.0.0.1:5000/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666125224286,\"created\":1666124761144,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_e581f2420345418c84d71dbed226b6da\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666125710431,\"created\":1666125184534,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d48cf679c2664c9bb566b600634b966f\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902939274,\"created\":1666124761145,\"url\":\"http://127.0.0.1:5000/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": \\\"f48f94a4760e40d39debf155396a9dec\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8982d9bcce734f60a9f27a8eb1fc748c\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666125332019,\"created\":1666124928966,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_99fbb0c34cd049f1bb8ac4e944f0ae6d\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902838552,\"created\":1666125104208,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_6b3e8bb38d0c4154826d63642b863687\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761125,\"created\":1666124761125,\"name\":\"Base Environment\",\"data\":{},\"dataPropertyOrder\":null,\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_9b95c15dadb44c03bf60cc7386095847\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761128,\"created\":1666124761128,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_cfb94f75feff4930966c80f350b1e115\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761155,\"created\":1666124761131,\"fileName\":\"Flask-Smorest\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/05_flask_smorest/_category_.json",
    "content": "{\n    \"label\": \"Flask-Smorest for More Efficient Development\",\n    \"position\": 5\n}\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/01_project_overview_sqlalchemy/README.md",
    "content": "---\ntitle: Project Overview, and why use SQLAlchemy\ndescription: Let's look at what we'll do in this section. There are no changes to the client-facing API at all, just changes internally to how we store data.\nctslug: project-overview-why-use-sqlalchemy\n---\n\n# Project Overview (and why use SQLAlchemy)\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\nIn this section we'll make absolutely no changes to the API! However, we will completely change the way we store data.\n\nUp until now, we've been storing data in an \"in-memory database\": a couple of Python dictionaries. When we stop the app, the data is destroyed. This is obviously not great, so we want to move to a proper store that can keep data around between app restarts!\n\nWe'll be using a relational database for data storage, and there are many different options: SQLite, MySQL, PostgreSQL, and others.\n\nAt this point we have two options regarding how to interact with the database:\n\n1. We can write SQL code and execute it ourselves. For example, when we want to add an item to the database we'd write something like `INSERT INTO items (name, price, store_id) VALUES (\"Chair\", 17.99, 1)`.\n2. We can use an ORM, which can take Python objects and turn them into database rows.\n\nFor this project, we are going to use an ORM because it makes the code much cleaner and simpler. Also, the ORM library (SQLAlchemy) helps us with many potential issues with using SQL, such as:\n\n- Multi-threading support\n- Handling creating the tables and defining the rows\n- Database migrations (with help of another library, Alembic)\n- Like mentioned, it makes the code cleaner, simpler, and shorter\n\nTo get started, add the following to the `requirements.txt` file:\n\n```text title=\"requirements.txt\"\nsqlalchemy\nflask-sqlalchemy\n```\n\n<details>\n  <summary>What is Flask-SQLAlchemy?</summary>\n  <div>\n    <p>SQLAlchemy is the ORM library, that helps map Python classes to database tables and columns, and turns Python objects of those classes into specific rows.</p>\n    <p>Flask-SQLAlchemy is a Flask extension which helps connect SQLAlchemy to Flask apps.</p>\n  </div>\n</details>\n\nWith this, install your requirements (remember to activate your virtual environment first!).\n\n```\npip install -r requirements.txt\n```\n\nLet's begin creating our SQLAlchemy models in the next lecture."
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/README.md",
    "content": "---\ntitle: Create a simple SQLAlchemy Model\ndescription: Lecture description goes here.\nctslug: create-a-simple-sqlalchemy-model\n---\n\n# Create a simple SQLAlchemy Model\n\n## Initialize the SQLAlchemy instance\n\n```python title=\"db.py\"\nfrom flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n```\n\n## Create models without relationships\n\nEvery model inherits from `db.Model`. That way when we tell SQLAlchemy about them (in [Configure Flask-SQLAlchemy](../configure_flask_sqlalchemy))), it will know to look at them to create tables.\n\nEvery model also has a few properties that let us interact with the database through the model, such as `query` (more on this in [Insert models in the database with SQLAlchemy](../insert_models_sqlalchemy)).\n\n```python title=\"models/item.py\"\nfrom db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, unique=False, nullable=False)\n```\n\n```python title=\"models/store.py\"\nfrom db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n```"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, unique=False, nullable=False)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Int(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Str(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/README.md",
    "content": "---\ntitle: One-to-many relationships with SQLAlchemy\ndescription: Model relationships let us easily retrieve information about a related model, without having to do SQL JOINs manually.\nctslug: one-to-many-relationships-with-sqlalchemy\n---\n\n# One-to-many relationships with SQLAlchemy\n\n```python title=\"models/item.py\"\nfrom db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    # highlight-start\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    # highlight-end\n```\n\n```python title=\"models/store.py\"\nfrom db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    # highlight-start\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    # highlight-end\n```\n\nTo make it easier to import and use the models, I'll also create a `models/__init__.py` file that imports the models from their files:\n\n```python title=\"models/__init__.py\"\nfrom models.store import StoreModel\nfrom models.item import ItemModel\n```\n\n## What is `lazy=\"dynamic\"`?\n\nWithout `lazy=\"dynamic\"`, the `items` attribute of the `StoreModel` resolves to a list of `ItemModel` objects.\n\nWith `lazy=\"dynamic\"`, the `items` attribute resolves to a SQLAlchemy **query**, which has some benefits and drawbacks:\n\n- A key benefit is load speed. Because SQLAlchemy doesn't have to go to the `items` table and load items, stores will load faster.\n- A key drawback is accessing the `items` of a store isn't as easy.\n  - However this has another hidden benefit, which is that when you _do_ load items, you can do things like filtering before loading.\n\nHere's how you could get all the items, giving you a list of `ItemModel` objects. Assume `store` is a `StoreModel` instance:\n\n```python\nstore.items.all()\n```\n\nAnd here's how you would do some filtering:\n\n```python\nstore.items.filter_by(name==\"Chair\").first()\n```\n\n## Updating our marshmallow schemas\n\nNow that the models have these relationships, we can modify our marshmallow schemas so they will return some or all of the information about the related models.\n\nWe do this with the `Nested` marshmallow field.\n\n:::caution\nSomething to be careful about is having schema A which has a nested schema B, which has a nested schema A.\n\nThis will lead to an infinite nesting, which is obviously never what you want!\n:::\n\nTo avoid infinite nesting, we are renaming our schemas which _don't_ use nested fields to `Plain`, such as `PlainItemSchema` and `PlainStoreSchema`.\n\nThen the schemas that _do_ use nesting can be called `ItemSchema` and `StoreSchema`, and they inherit from the plain schemas. This reduces duplication and prevents infinite nesting.\n\n```python title=\"schemas.py\"\nfrom marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n```"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, unique=False, nullable=False)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/README.md",
    "content": "---\ntitle: Configure Flask-SQLAlchemy\ndescription: Link Flask-SQLAlchemy with our Flask app and create the initial tables.\nctslug: configure-flask-sqlalchemy\n---\n\n# Configure Flask-SQLAlchemy\n\nWe want to add two imports to `app.py`:\n\n```python title=\"app.py\"\nfrom db import db\n\nimport models\n```\n\n## The Flask app factory pattern\n\nUp until now, we've been creating the `app` variable (which is the Flask app) directly in `app.py`.\n\nWith the app factory pattern, we write a function that _returns_ `app`. That way we can _pass configuration values_ to the function, so that we configure the app before getting it back.\n\nThis is especially useful for testing, but also if you want to do things like have staging and production apps.\n\nTo do the app factory, all we do is place all the app-creation code inside a function which **must be called `create_app()`**.\n\n```python title=\"app.py\"\nfrom flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n# highlight-start\ndef create_app():\n    app = Flask(__name__)\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    api = Api(app)\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n# highlight-end\n```\n\n## Add Flask-SQLAlchemy code to the app factory\n\n```python title=\"app.py\"\nimport os\n\nfrom flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n# highlight-start\ndef create_app(db_url=None):\n    # highlight-end\n    app = Flask(__name__)\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    # highlight-start\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    # highlight-end\n    api = Api(app)\n\n    # highlight-start\n    with app.app_context():\n        db.create_all()\n    # highlight-end\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n```\n\nWe've done three things:\n\n1. Added the `db_url` parameter. This lets us create an app with a certain database URL, or alternatively try to fetch the database URL from the environment variables. The default value will be a local SQLite file, if we don't pass a value ourselves and it isn't in the environment.\n2. Added two SQLAlchemy values to `app.config`. One is the database URL (or URI), the other is a [configuration option](https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/) which improves performance.\n3. When the app is created, tell SQLAlchemy to create all the database tables we need.\n\n:::tip How does SQLAlchemy know what tables to create?\nThe line `import models` lets SQLAlchemy know what models exist in our application. Because they are `db.Model` instances, SQLAlchemy will look at their `__tablename__` and defined `db.Column` attributes to create the tables.\n:::\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/README.md",
    "content": "---\ntitle: Insert models in the database with SQLAlchemy\ndescription: Learn how to use SQLAlchemy to add new rows to our SQL database.\nctslug: insert-models-in-database-with-sqlalchemy\n---\n\n# Insert models in the database with SQLAlchemy\n\nInserting models with SQLAlchemy couldn't be easier! We'll use the `db.session`[^1] variable to `.add()` a model. Let's begin working on our `Item` resource:\n\n```python title=\"resources/item.py\"\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\n\n...\n\n@blp.arguments(ItemSchema)\n@blp.response(201, ItemSchema)\ndef post(self, item_data):\n    item = ItemModel(**item_data)\n\n    try:\n        db.session.add(item)\n        db.session.commit()\n    except SQLAlchemyError:\n        abort(500, message=\"An error occurred while inserting the item.\")\n\n    return item\n```\n\nSimilarly in our `Store` resource:\n\n```python title=\"resources/store.py\"\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\n\n...\n\n@blp.arguments(StoreSchema)\n@blp.response(201, StoreSchema)\ndef post(self, store_data):\n    store = StoreModel(**store_data)\n    try:\n        db.session.add(store)\n        db.session.commit()\n    except IntegrityError:\n        abort(\n            400,\n            message=\"A store with that name already exists.\",\n        )\n    except SQLAlchemyError:\n        abort(500, message=\"An error occurred creating the store.\")\n\n    return store\n```\n\nNote here we're catching two different errors, `IntegrityError` for when a client attempts to create a store with a name that already exists, and `SQLAlchemyError` for anything else.\n\nSince the `StoreModel`'s `name` column is marked as `unique=True`, then an `IntegrityError` is raised when we try to insert another row with the same name.\n\n[^1]: [Session Basics (SQLAlchemy Documentation)](https://docs.sqlalchemy.org/en/14/orm/session_basics.html)"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        raise NotImplementedError(\"Creating an item is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        raise NotImplementedError(\"Creating a store is not implemented.\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/README.md",
    "content": "---\ntitle: Get models by ID from the database using SQLAlchemy\ndescription: Learn how to fetch a specific model using its primary key column, and how to return a 404 page if it isn't found.\nctslug: get-models-by-id-from-the-database\n---\n\n# Get models by ID from the database using SQLAlchemy\n\nUsing the model class's `query` attribute, we have access to two very handy methods:\n\n- `ItemModel.query.get(item_id)` gives us an `ItemModel` object from the database where the `item_id` matches the primary key.\n- `ItemModel.query.get_or_404(item_id)` does the same, but makes Flask immediately return a \"Not Found\" message, together with a 404 error code, if no model can be found with that ID in the database.\n\n:::tip\nWhen we use `.get_or_404()` and nothing is found, this is the response from the API:\n\n```json\n{\"code\": 404, \"status\": \"Not Found\"}\n```\n\nThe status code of this response is also 404.\n:::\n\nWe're going to use `.get_or_404()` repeatedly in our resources!\n\nFor now, and since we'll need an `ItemModel` instance in all our `Item` resource methods, let's add that:\n\n```python title=\"resources/item.py\"\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        # highlight-start\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n        # highlight-end\n\n    def delete(self, item_id):\n        # highlight-start\n        item = ItemModel.query.get_or_404(item_id)\n        # highlight-end\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        # highlight-start\n        item = ItemModel.query.get_or_404(item_id)\n        # highlight-end\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n```\n\nSimilarly in our `Store` resource:\n\n```python title=\"resources/store.py\"\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        # highlight-start\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n        # highlight-end\n\n    def delete(self, store_id):\n        # highlight-start\n        store = StoreModel.query.get_or_404(store_id)\n        # highlight-end\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n```\n\nWith this, we're ready to continue!"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        raise NotImplementedError(\"Getting an item is not implemented.\")\n\n    def delete(self, item_id):\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        raise NotImplementedError(\"Getting a store is not implemented.\")\n\n    def delete(self, store_id):\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/README.md",
    "content": "---\ntitle: Updating models with SQLAlchemy\ndescription: How to make changes to an existing model, or insert one if it doesn't already exist.\nctslug: updating-models-with-sqlalchemy\n---\n\n# Updating models with SQLAlchemy\n\nA frequent operation in REST APIs is the \"upsert\", or \"update or insert\".\n\nThis is an idempotent operation where we send the data we want the API to store. If the data identifier already exists, an update is done. If it doesn't, it is created.\n\nThis idempotency is frequently seen with `PUT` requests. You can see it in action here:\n\n```python title=\"resources/item.py\"\n@blp.arguments(ItemUpdateSchema)\n@blp.response(200, ItemSchema)\ndef put(self, item_data, item_id):\n    # highlight-start\n    item = ItemModel.query.get(item_id)\n    if item:\n        item.price = item_data[\"price\"]\n        item.name = item_data[\"name\"]\n    else:\n        item = ItemModel(id=item_id, **item_data)\n\n    db.session.add(item)\n    db.session.commit()\n\n    return item\n    # highlight-end\n```\n\nOur `ItemUpdateSchema` at the moment looks like this:\n\n```python title=\"schemas.py\"\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n```\n\nBut since now our update endpoint may create items, we need to change the schema to optionally accept a `store_id`.\n\nWhen updating an item, `name` or `price` (or both) may be passed, but when creating an item, `name`, `price`, and `store_id` must be passed.\n\nUpdate the `ItemUpdateSchema` to this:\n\n```python title=\"schemas.py\"\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n```\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        raise NotImplementedError(\"Updating an item is not implemented.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/README.md",
    "content": "---\ntitle: Retrieve a list of all models\ndescription: Get more than one model and return it as a list from the API.\nctslug: retrieve-a-list-of-all-models\n---\n\n# Retrieve a list of all models\n\nUsing the `query` attribute of our model class, we can retrieve all the results of the query:\n\n```python title=\"resources/item.py\"\n@blp.response(200, ItemSchema(many=True))\ndef get(self):\n    # highlight-start\n    return ItemModel.query.all()\n    # highlight-end\n```\n\n```python title=\"resources/store.py\"\n@blp.response(200, StoreSchema(many=True))\ndef get(self):\n    # highlight-start\n    return StoreModel.query.all()\n    # highlight-end\n```"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing items is not implemented.\")\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema, ItemSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        raise NotImplementedError(\"Listing stores is not implemented.\")\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/README.md",
    "content": "---\ntitle: Delete models with SQLAlchemy\ndescription: Use SQLAlchemy to handle removal of a specific model.\nctslug: delete-models-with-sqlalchemy\n---\n\n# Delete models with SQLAlchemy\n\nJust as with adding, deleting models is a matter of using `db.session`, and then committing when the deletion is complete:\n\n```python title=\"resources/item.py\"\ndef delete(self, item_id):\n    item = ItemModel.query.get_or_404(item_id)\n    # highlight-start\n    db.session.delete(item)\n    db.session.commit()\n    return {\"message\": \"Item deleted.\"}\n    # highlight-end\n```\n\n```python title=\"resources/store.py\"\ndef delete(self, store_id):\n    store = StoreModel.query.get_or_404(store_id)\n    # highlight-start\n    db.session.delete(store)\n    db.session.commit()\n    return {\"message\": \"Store deleted\"}, 200\n    # highlight-end\n```"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        raise NotImplementedError(\"Deleting an item is not implemented.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        raise NotImplementedError(\"Deleting a store is not implemented.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/README.md",
    "content": "---\ntitle: Delete models with relationships using cascades\ndescription: Tell SQLAlchemy what to do with related models when you delete the parent.\nctslug: delete-models-with-relationships-using-cascades\n---\n\n# Delete models with relationships using cascades\n\nWhen you delete a model that has a relationship to other models that still exist, the default behavior in SQLAlchemy with PostgreSQL is to raise an error. This is because SQLAlchemy does not want to allow you to accidentally delete data that is still being used by other models.\n\nLet's say you have a `Store 1` that has two items, `Item 1` and `Item 2`. If you try to delete Store 1 without first deleting Item 1 and Item 2, SQLAlchemy will raise an error because the items are still related to the store.\n\nThis means the items have a **Foreign Key** that references the store you're trying to delete. If the store actually was deleted, the items have a store ID that references something that doesn't exist.\n\nTo fix this, you can use a feature called \"cascading deletes\". Cascading deletes allow you to specify that when a model is deleted, any related models should also be deleted automatically.\n\nSQLAlchemy makes it easy to add cascades to our models, here's how you might do that!\n\n```python title=\"models/store.py\"\nfrom db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    # highlight-start\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\", cascade=\"all, delete\")\n    # highlight-end\n```\n\nRemember that `StoreModel` and `ItemModel` have a one-to-many relationship, where each store can have multiple items, and each item belongs to a single store.\n\nThe `cascade=\"all,delete\"` argument in the `relationship()` call for the `StoreModel.items` attribute specifies that when a store is deleted, all of its related items should also be deleted.\n\nIf you add a `cascade` on the relationship in the `ItemModel`, then when an item is deleted, its related store should also be deleted. This is not what we want, so we won't add a cascade to `ItemModel`.\n\nWith this code in place, if you try to delete a store that still has items, the items will be deleted automatically along with the store. This will allow you to delete the store without having to delete the items individually.\n\nFor more information, I strongly recommend reading [the official documentation](https://docs.sqlalchemy.org/en/20/orm/cascades.html#delete)! There are also other cascade options you can pass in depending on what you want to happen to related models when the parent changes or is deleted."
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\n        \"ItemModel\", back_populates=\"store\", lazy=\"dynamic\", cascade=\"all, delete\"\n    )\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/requirements.txt",
    "content": "flask\nflask-sqlalchemy\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/11_conclusion/README.md",
    "content": "---\ntitle: Conclusion of this section\ndescription: Review everything we've changed this section to add SQL storage with SQLAlchemy to our API.\nctslug: conclusion-of-this-section\n---\n\n# Conclusion of this section\n\nAdding SQL storage to our app has required quite a few changes! Let's do a quick review.\n\n## Installed SQLAlchemy and Flask-SQLAlchemy\n\n```\npip install sqlalchemy flask-sqlalchemy\n```\n\nAnd\n\n```text title=\"requirements.txt\"\nsqlalchemy\nflask-sqlalchemy\n```\n\n## Created models\n\n```python title=\"models/item.py\"\nfrom db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n```\n\nAnd\n\n```python title=\"models/store.py\"\nfrom db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n```\n\n## Updated resources to use SQLAlchemy\n\nPreviously we were using Python dictionaries as a database. Now we've swapped them out for using SQLAlchemy models by:\n\n- Importing the models in our resource files\n- Retrieving models from the database with `ModelClass.query.get_or_404(model_id)`.\n- Updating models by changing attributes, or creating new model class instances, and then saving and committing with `db.session.add(model_instance)` and `db.session.commit()`.\n- Deleting models with `db.session.delete(model_instance)` followed by `db.session.commit()`.\n\n## Updated marshmallow schemas\n\nSince now our models have relationships, that means that the schemas can have `Nested` fields.\n\nThe schemas that don't have `Nested` fields we've called \"Plain\" schemas, and those that do are named after the model they represent.\n\n```python title=\"schemas.py\"\nfrom marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n```\n\nAnd that's it! Quite a few changes, but hopefully you're still with me.\n\nIn the following sections we'll be adding more functionality to our API, so stay tuned!"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/Insomnia_section6.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:30:25.805Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_8612530e54144a039af84006ee8c882d\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689179,\"created\":1666987689179,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689178,\"created\":1666987689178,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"parentId\":null,\"modified\":1666987689171,\"created\":1666987689171,\"name\":\"Section 6\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_335002433e9745068d074f1f942ddde2\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689183,\"created\":1666987689183,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4f7b9d616b0e44ca94ca51cc71660da0\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666990320166,\"created\":1666987689181,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9228903cf7a54601a51a59f6a6692363\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689184,\"created\":1666987689184,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d1d499ead63e469ca04571899cc4759f\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689190,\"created\":1666987689190,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689186,\"created\":1666987689186,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_94738c7e8c774bd597ffe97bf7b921b6\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689194,\"created\":1666987689194,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0624b67ef6b841f482b7e7522fb6f405\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666990328367,\"created\":1666987689187,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b2651043ea5e4b33b073f260712fb114\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689189,\"created\":1666987689189,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_640f313dbd5a4bfcbf98081e2fab6d4a\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689192,\"created\":1666987689192,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_892efa21f8454221972d0c77a336872c\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689172,\"created\":1666987689172,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_aff586a35c4c49aa91c5defb067355bf\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689174,\"created\":1666987689174,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_4e7424f78749436bacdb44d3a1eba77a\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689205,\"created\":1666987689176,\"fileName\":\"Section 6\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/06_sql_storage_sqlalchemy/_category_.json",
    "content": "{\n    \"label\": \"SQL Storage with Flask-SQLAlchemy\",\n    \"position\": 6\n}\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/01_section_changes/README.md",
    "content": "---\ntitle: Changes in this section\ndescription: In this section we add Tags to our Stores, and link these to Items using a many-to-many relationship.\nctslug: changes-in-many-to-many-section\n---\n\n# Changes in this section\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\nIt's common for online stores to use \"tags\" to group items and to be able to search for them a bit more easily.\n\nFor example, an item \"Chair\" could be tagged with \"Furniture\" and \"Office\".\n\nAnother item, \"Laptop\", could be tagged with \"Tech\" and \"Office\".\n\nSo one item can be associated with many tags, and one tag can be associated with many items.\n\nThis is a many-to-many relationship, which is bit trickier to implement than the one-to-many we've already implemented between Items and Stores.\n\n## When you have many stores\n\nWe want to add one more constraint to tags, however. That is that if we have many stores, it's possible each store wants to use different tags. So the tags we create will be unique to each store.\n\nThis means that tags will have:\n\n- A many-to-one relationship with stores\n- A many-to-many relationship with items\n\nHere's a diagram to illustrate what this looks like:\n\n![ER database model showing relationships](https://res.cloudinary.com/teclado/image/upload/v1689180742/courses/rest-apis-flask-python/db_model.drawio_ilgupm.png)\n\n## New API endpoints to be added\n\nIn this section we will add all the Tag endpoints:\n\n\n| Method   | Endpoint              | Description                                             |\n| -------- | --------------------- | ------------------------------------------------------- |\n| `GET`    | `/store/{id}/tag`     | Get a list of tags in a store.                          |\n| `POST`   | `/store/{id}/tag`     | Create a new tag.                                       |\n| `POST`   | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |\n| `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item.                              |\n| `GET`    | `/tag/{id}`           | Get information about a tag given its unique id.        |\n| `DELETE` | `/tag/{id}`           | Delete a tag, which must have no associated items.      |"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/README.md",
    "content": "---\ntitle: One-to-many relationships review\ndescription: A super-quick look at creating the Tag model and setting up the one-to-many relationship with Stores.\nctslug: one-to-many-relationship-between-tag-store\n---\n\n# One-to-many relationship between Tag and Store\n\nSince we've already learned how to set up one-to-many relationships with SQLAlchemy when we looked at Items and Stores, let's go quickly in this section.\n\n## The SQLAlchemy models\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"tag\" label=\"models/tag.py\" default>\n\n```python title=\"models/tag.py\"\nfrom db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n```\n\n</TabItem>\n<TabItem value=\"store\" label=\"models/store.py\">\n\n```python title=\"models/store.py\"\nfrom db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    # highlight-start\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    # highlight-end\n```\n\n</TabItem>\n</Tabs>\n</div>\n\nRemember to import the `TagModel` in `models/__init__.py` so that it is then imported by `app.py`. Otherwise SQLAlchemy won't know about it, and it won't be able to create the tables.\n\n## The marshmallow schemas\n\nThese are the new schemas we'll add. Note that none of the tag schemas have any notion of \"items\". We'll add those to the schemas when we construct the many-to-many relationship.\n\nIn the `StoreSchema` we add a new list field for the nested `PlainTagSchema`, just as it has with `PlainItemSchema`.\n\n```python title=\"schemas.py\"\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    # highlight-start\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n    # highlight-end\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n```\n\n## The API endpoints\n\nLet's add the Tag endpoints that aren't related to Items:\n\n| Method     | Endpoint              | Description                                             |\n| ---------- | --------------------- | ------------------------------------------------------- |\n| ✅ `GET`    | `/store/{id}/tag`     | Get a list of tags in a store.                          |\n| ✅ `POST`   | `/store/{id}/tag`     | Create a new tag.                                       |\n| ❌ `POST`   | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |\n| ❌ `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item.                              |\n| ✅ `GET`    | `/tag/{id}`           | Get information about a tag given its unique id.        |\n| ❌ `DELETE` | `/tag/{id}`           | Delete a tag, which must have no associated items.      |\n\nHere's the code we need to write to add these endpoints:\n\n```python title=\"resources/tag.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel\nfrom schemas import TagSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n```\n\n## Register the Tag blueprint in `app.py`\n\nFinally, we need to remember to import the blueprint and register it!\n\n```python title=\"app.py\"\nfrom flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n# highlight-start\nfrom resources.tag import blp as TagBlueprint\n# highlight-end\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    # highlight-start\n    api.register_blueprint(TagBlueprint)\n    # highlight-end\n\n    return app\n```\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel\nfrom schemas import TagSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/README.md",
    "content": "---\ntitle: Many-to-many relationships\ndescription: Learn to set up a many-to-many relationship between two models using SQLAlchemy.\nctslug: many-to-many-relationships\n---\n\n# Many-to-many relationships\n\n## The SQLAlchemy models\n\nIn one-to-many relationships, one of the models has a foreign key that links it to another model. \n\nHowever, for a many-to-many relationship, one model can't have a single value as a foreign key (otherwise it would be a one-to-many!). Instead, what we do is construct a **secondary table** that has, in each row, a tag ID and and item ID.\n\n| id  | tag_id | item_id |\n| --- | ------ | ------- |\n| 1   | 2      | 5       |\n| 2   | 1      | 4       |\n| 3   | 4      | 5       |\n| 4   | 1      | 3       |\n\n<details>\n  <summary>Explanation of the table above</summary>\n  <div>\n    <p>The table above has 4 rows, which tell us the following:</p>\n    <ol>\n        <li>Tag with ID <code>1</code> is linked to Items with IDs <code>3</code> and <code>4</code>.</li>\n        <li>Tag with ID <code>2</code> is linked to Item with ID <code>5</code>.</li>\n        <li>Tag with ID <code>4</code> is linked to Item with ID <code>5</code>.</li>\n    </ol>\n    <p>And therefore:</p>\n    <ol>\n        <li>Item with ID <code>3</code> is linked to Tag with ID <code>1</code>.</li>\n        <li>Item with ID <code>4</code> is linked to Tag with ID <code>1</code>.</li>\n        <li>Item with ID <code>5</code> is linked to Tags with IDs <code>2</code> and <code>4</code>.</li>\n    </ol>\n    <p>This is how many-to-many relationships work, and through this secondary table, the <code>Tag.items</code> and <code>Item.tags</code> attributes will be populated by SQLAlchemy.</p>\n  </div>\n</details>\n\nThe rows in this table then signify a link between a specific tag and a specific item, but without the need for those values to be stored in the tag or item models themselves.\n\n### Writing the secondary table for many-to-many relationships\n\nAs we've just seen, many-to-many relationships use a secondary table which stores which models of one side are related to which models of the other side.\n\nJust as we did with `Item`, `Store`, and `Tag`, we'll create a model for this secondary table:\n\n```python title=\"models/item_tags.py\"\nfrom db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n```\n\nLet's also add this to our `models/__init__.py` file:\n\n```python title=\"models/__init__.py\"\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n```\n\n### Using the secondary table in the main models\n\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"tag\" label=\"models/tag.py\" default>\n\n```python title=\"models/tag.py\"\nfrom db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    # highlight-start\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n    # highlight-end\n```\n\n</TabItem>\n<TabItem value=\"item\" label=\"models/item.py\">\n\n```python title=\"models/item.py\"\nfrom db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    # highlight-start\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n    # highlight-end\n```\n\n</TabItem>\n</Tabs>\n</div>\n\n## The marshmallow schemas\n\nNext up, let's add the nested fields to the marshmallow schemas.\n\nThe `TagAndItemSchema` will be used to return information about both the Item and Tag that have been modified in an endpoint, together with an informative message.\n\n```python title=\"schemas.py\"\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    # highlight-start\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n    # highlight-end\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    # highlight-start\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    # highlight-end\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n# highlight-start\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n# highlight-end\n```\n\n## The API endpoints\n\nNow let's add the rest of our API endpoints (grayed out are the ones we implemented in [one-to-many relationships review](../one_to_many_review/))!\n\n| Method                                         | Endpoint                                                | Description                                                                            |\n| ---------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| <span style={{opacity: \"50%\"}}>✅ `GET`</span>  | <span style={{opacity: \"50%\"}}>`/store/{id}/tag`</span> | <span style={{opacity: \"50%\"}}>Get a list of tags in a store.</span>                   |\n| <span style={{opacity: \"50%\"}}>✅ `POST`</span> | <span style={{opacity: \"50%\"}}>`/store/{id}/tag`</span> | <span style={{opacity: \"50%\"}}>Create a new tag.</span>                                |\n| ✅ `POST`                                       | `/item/{id}/tag/{id}`                                   | Link an item in a store with a tag from the same store.                                |\n| ✅ `DELETE`                                     | `/item/{id}/tag/{id}`                                   | Unlink a tag from an item.                                                             |\n| <span style={{opacity: \"50%\"}}>✅ `GET`</span>  | <span style={{opacity: \"50%\"}}>`/tag/{id}`</span>       | <span style={{opacity: \"50%\"}}>Get information about a tag given its unique id.</span> |\n| ✅ `DELETE`                                     | `/tag/{id}`                                             | Delete a tag, which must have no associated items.                                     |\n\nHere's the code (new lines highlighted):\n\n```python title=\"resources/tag.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\n# highlight-start\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n# highlight-end\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n# highlight-start\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n# highlight-end\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    # highlight-start\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )\n    # highlight-end\n```\n\nAnd with that, we're done!\n\n## Making sure Store ID matches when linking tags\n\nIf you wanted to, you can make sure that you can only link a tag that belongs to a certain store, with an item of that same store.\n\nSomething like this would work:\n\n```py\nif item.store.id != tag.store.id:\n    abort(400, message=\"Make sure item and tag belong to the same store before linking.\")\n```\n\nNow we're ready to look at securing API endpoints with user authentication."
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel\nfrom schemas import TagSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/Insomnia_section7.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:30:50.558Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_379d0e42420f466bbad1b7481e5e7816\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991794866,\"created\":1666990973919,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_86b5e8072a894c409febe46716e99809\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990939045,\"created\":1666990939045,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"parentId\":null,\"modified\":1666990745588,\"created\":1666990745588,\"name\":\"Section 7\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_85adfd198935497bb7aedb266beb5bf3\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991788350,\"created\":1666990945502,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4765f7ca8e1e46308cdde255d09a2ffc\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991810641,\"created\":1666991378432,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f07aab6ead044ca7bba0de3437ab08c4\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991779049,\"created\":1666991031108,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_77d1a5f225c54acbb27bac15010722ad\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991824192,\"created\":1666991489163,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d60510ab22b2499abb20a63629e30fcd\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991828682,\"created\":1666991524256,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ab3c728a796e4b4ca51803248e1b0650\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745596,\"created\":1666990745596,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_597937a09435404ebe2200cbaeed101d\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745596,\"created\":1666990745596,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"req_a9d43bb23e1246da94aec50b9b9ca652\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745601,\"created\":1666990745601,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8a36225a08bb4dfbbf98fd983b0d4a5f\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666991654175,\"created\":1666990745599,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_303507538c0f408eb6d91784b7ed8d36\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745602,\"created\":1666990745602,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f4776751aecc4c6eafb264dc2d2c24cb\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745609,\"created\":1666990745609,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745604,\"created\":1666990745604,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_e6bc2422c8cf4f119c7dc10251a9af65\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745611,\"created\":1666990745611,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6c72c92f81924ce7bc26ceb488fd64ff\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666991658886,\"created\":1666990745605,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e86e0877045640d690454a99b176f3a2\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745607,\"created\":1666990745607,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f0c4a3d747a543249131e19ceea79e56\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745610,\"created\":1666990745610,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_7609e8f1315a4d77af52a6ba50f48205\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745590,\"created\":1666990745590,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_ce9759718e054191a685cec521ed7afc\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745592,\"created\":1666990745592,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_0f62897a05a449f9845b4c71eeb892b3\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745620,\"created\":1666990745594,\"fileName\":\"Section 7\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/07_sqlalchemy_many_to_many/_category_.json",
    "content": "{\n    \"label\": \"Many-to-many relationships with SQLAlchemy\",\n    \"position\": 7\n}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/01_section_changes/README.md",
    "content": "---\ntitle: Changes in this section\ndescription: Overview of the API endpoints we'll use for user registration and authentication.\nctslug: changes-in-jwt-extended-section\n---\n\n# Changes in this section\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\nIn this section we will add the following endpoints:\n\n| Method         | Endpoint          | Description                                           |\n| -------------- | ----------------- | ----------------------------------------------------- |\n| `POST`         | `/register`       | Create user accounts given an `email` and `password`. |\n| `POST`         | `/login`          | Get a JWT given an `email` and `password`.            |\n| 🔒 <br/> `POST` | `/logout`         | Revoke a JWT.                                         |\n| 🔒 <br/> `POST` | `/refresh`        | Get a fresh JWT given a refresh JWT.                  |\n| `GET`          | `/user/{user_id}` | (dev-only) Get info about a user given their ID.      |\n| `DELETE`       | `/user/{user_id}` | (dev-only) Delete a user given their ID.              |\n\nWe will also protect some existing endpoints by requiring a JWT from clients. You can see which endpoints will be protected in [The API we'll build in this course](/docs/course_intro/what_is_rest_api/#the-api-well-build-in-this-course)"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/02_what_is_a_jwt/README.md",
    "content": "---\ntitle: What is a JWT?\ndescription: Understand what a JWT is, what data it contains, and how it may be used.\nctslug: what-is-a-jwt\n---\n\n# What is a JWT?\n\nA JWT is a signed JSON object with a specific structure. Our Flask app will sign the JWTs with the secret key, proving that _it generated them_.\n\nThe Flask app generates a JWT when a user logs in (with their username and password). In the JWT, we'll store the user ID. The client then stores the JWT and sends it to us on every request.\n\nBecause we can prove our app generated the JWT (through its signature), and we will receive the JWT with the user ID in every request, we can _treat requests that include a JWT as \"logged in\"_.\n\nFor example, if we want certain endpoints to only be accessible to logged-in users, all we do is require a JWT in them. Since the client can only get a JWT after logging in, we know that including a JWT is proof that the client logged in successfully at some point in the past.\n\nAnd since the JWT includes the user ID inside it, when we receive a JWT we know _who logged in_ to get the JWT.\n\nThere's a lot more information about JWTs here: [https://jwt.io/introduction](https://jwt.io/introduction). This includes information such as:\n\n- What is stored inside a JWT?\n- Are JWTs secure?\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/03_how_is_jwt_used/README.md",
    "content": "---\ntitle: How are JWTs used?\ndescription: Learn who uses JWTs and how they are used by clients and servers to perform authentication.\nctslug: how-are-jwts-used\n---\n\n# How are JWTs used?\n\n:::info JWT vs. Access token?\nAn \"access token\" is any piece of information that a client can use to authenticate. In this API, we use JWTs. Therefore you can say that the JWT and the access token are one and the same!\n:::\n\nWe've learned that a JWT is generated by the API and sent to the client. When the client wants to login they will send the API information that allows them to do so: usually, the user's username and password. The API then validates that this login information is correct, and generates the access token.\n\nInside the access token, the API stores identifying information for the user. Then the access token is sent to the client who stores it in whichever way they see fit. In every subsequent request to the API, the client should include the access token. That way, just with that information, the API can tell _who_ made the request. The API can decode the access token and see inside it the identifying information for the user for whom the access token was generated.\n\nHere is a diagram of the interaction between client and API to generate an access token:\n\n<div style={{maxWidth: '600px'}}>\n\n![Diagram showing the flow between client and server to generate an access token](https://res.cloudinary.com/teclado/image/upload/v1689180750/courses/rest-apis-flask-python/access-token-flow.drawio_gdr9oo.png)\n\n</div>\n\n## An example of using access tokens\n\nFor example, let's say you want to make an API that has an endpoint `/my-info`. This endpoint should send the client information about the currently logged-in user.\n\nLet's imagine that **the client** is a website. In the website, there is a button, \"See my info\", which when clicked sends a request to the API's `/my-info` endpoint to get the logged-in user's information.\n\n### Clicking the button without logging in\n\nIf the user navigates to the website and clicks the \"See my info\" button, the website will send a request to the API. Because the user hasn't logged in yet, the website doesn't have an access token generated for this user.\n\nTherefore, the API responds with an \"authentication error\".\n\nThe website receives the authentication error and that tells it that the user hasn't logged in. So the website can show the user a log-in form, for the user to enter their username and password.\n\nWhen the user enters their username and password, the website will send a request to the API's `/login` endpoint. The API then responds with the access token. The website stores the access token for use later.\n\nIf the user clicks the \"See my info\" button again, now the website will include the access token in the request.\n\nThe server will then:\n\n1. See the access token.\n2. Decode it.\n3. Look at what user the access token was generated for.\n4. Load _that_ user's information from the database.\n5. Respond with that user's information.\n\nThe website receives the user's information, and can display it.\n\nThis is why the user sees their own information, and not someone else's. The access token was generated after they logged in with their details, and the access token stores their user ID. The server will use that to retrieve the correct data.\n\nHere is a rather long diagram depicting what happens:\n\n<div style={{maxWidth: '600px'}}>\n\n![Diagram showing flow of data when user wants to load their information but aren't logged in](https://res.cloudinary.com/teclado/image/upload/v1689180750/courses/rest-apis-flask-python/my-info-flow.drawio_pzfjh7.png)\n\n</div>\n\n:::warning This course deals only with the API\nRemember that in this course, we're making the API. We are not concerned with the client! We don't care how the client stores the access token or even whether the client is a website, mobile app, Postman or Insomnia, or anything else!\n:::\n\n## When do users provide their username and password?\n\nAccess tokens don't last forever: they normally have expiry times within 30 days of being generated. The shorter the expiry time of an access token, the more often that the user has to re-authenticate by providing their username and password, but the more secure the token is.\n\nTokens are more secure if they expire sooner because if the user forgets to log out of a shared device, and someone else tries to use their account, the token will expire and they will be unable to use the account.\n\nObviously, it's not a great experience for users if they have to keep re-entering their username and password constantly. Towards the end of this section we will learn about [token refreshing](../12_token_refreshing_flask_jwt_extended/README.md), which is a way to reduce the amount of times users have to re-authenticate, without affecting security too much.\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/README.md",
    "content": "---\ntitle: Flask-JWT-Extended setup\ndescription: Install and set up the Flask-JWT-Extended extension with our REST API.\nctslug: flask-jwt-extended-setup\n---\n\n# Flask-JWT-Extended setup\n\nFirst, let's update our requirements:\n\n```diff title=\"requirements.txt\"\n+ flask-jwt-extended\n```\n\nThen we must do two things:\n\n- Add the extension to our `app.py`.\n- Set a secret key that the extension will use to _sign_ the JWTs.\n\n```python title=\"app.py\"\nfrom flask import Flask\nfrom flask_smorest import Api\n# highlight-start\nfrom flask_jwt_extended import JWTManager\n# highlight-end\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    # highlight-start\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n    # highlight-end\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n```\n\n:::caution\nThe secret key set here, `\"jose\"`, is **not very safe**.\n\nInstead you should generate a long and random secret key using something like `str(secrets.SystemRandom().getrandbits(128))`.\n:::\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\n        \"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\n        \"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/README.md",
    "content": "---\ntitle: The User model and schema\ndescription: Create the SQLAlchemy User model and marshmallow schema.\nctslug: the-user-model-and-schema\n---\n\n# The User model and schema\n\nJust as we did with items, stores, and tags, let's create two classes for our users:\n\n- The SQLAlchemy model, to interact with the database.\n- The marshmallow schema, to deserialize data from clients and serialize it back to return data.\n\n## The User SQLAlchemy model\n\n```python title=\"models/user.py\"\nfrom db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n```\n\nLet's also add this class to `models/__init__.py` so it can then be imported by `app.py`:\n\n```python title=\"models/__init__.py\"\nfrom models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n```\n\n## The User marshmallow schema\n\n```python title=\"schemas.py\"\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n```"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\n        \"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/README.md",
    "content": "---\ntitle: How to add a register endpoint to the REST API\ndescription: Learn how to add a registration endpoint to a REST API using Flask-Smorest and Flask-JWT-Extended.\nctslug: how-to-add-a-register-endpoint-to-the-rest-api\n---\n\n# How to add a register endpoint to the REST API\n\nRegistering users sounds like a conceptually very difficult thing, but let's break it down into steps:\n\n- Receive username and password from the client (as JSON).\n- Check if a user with that username already exists.\n- If it doesn't...\n  - Encrypt the password.\n  - Add a new `UserModel` to the database.\n  - Return a success message.\n\n## Boilerplate set-up for a blueprint with Flask-Smorest\n\nFirst, we need our imports and blueprint set-up. This is the same for pretty much every Flask-Smorest blueprint, so you already know how to do it!\n\n```python title=\"resources/user.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n```\n\n## Creating the `UserRegister` resource\n\nNow let's create the `MethodView` class, and register a route to it using the blueprint:\n\n```python title=\"resources/user.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n# highlight-start\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n# highlight-end\n```\n\n## Creating a testing-only `User` resource\n\nLet's also create a `User` resource that we will only use during testing. It allows us to retrieve information about a single user, or delete a user. This will be handy so that using Insomnia or Postman we can clear the registered users and we don't have to change our request arguments each time!\n\n```python title=\"resources/user.py\"\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n```\n\n## Register the user blueprint in `app.py`\n\nFinally, let's go to `app.py` and register the blueprint!\n\n```diff title=\"app.py\"\n+from resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n...\n\n+api.register_blueprint(UserBlueprint)\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\napi.register_blueprint(TagBlueprint)\n```"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/README.md",
    "content": "---\ntitle: How to add a login endpoint to the REST API\ndescription: Learn how to add a login endpoint that returns a JWT to a REST API using Flask-Smorest and Flask-JWT-Extended.\nctslug: how-to-add-a-login-endpoint\n---\n\n# How to add a login endpoint to the REST API\n\nNow that we've done registration, we can do log in! It's very similar.\n\nLet's import `flask_jwt_extended.create_access_token` so that when we receive a valid username and password from the client, we can create a JWT and send it back:\n\n```diff title=\"resources/user.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n+from flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n```\n\nThen let's create our `UserLogin` resource.\n\n```python title=\"resources/user.py\"\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n```\n\nHere you can see the when we call `create_access_token(identity=str(user.id))` we pass in the user's `id`. This is what gets stored (among other things) inside the JWT, so when the client sends the JWT back on every request, we can tell who the JWT belongs to.\n\n**Update Nov 2024**: Before now, we used `identity=user.id`, but now we have to convert it to a string first.\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/README.md",
    "content": "---\ntitle: Protect endpoints by requiring a JWT\ndescription: Use jwt_required from Flask-JWT-Extended to prevent unauthorised users from making requests to certain endpoints in a REST API.\nctslug: protect-endpoints-by-requiring-a-jwt\n---\n\n# Protect endpoints by requiring a JWT\n\nNow that our users can sign up and log in, that means we can start _requiring login_ for certain endpoints.\n\nAll this means in practice is that the client making the request must send a valid JWT.\n\nRemember, we can tell if a JWT is valid because it is _signed by our app_. If the user changes the JWT at all, the signature will be invalid, and we'll know it has been tampered with. Flask-JWT-Extended takes care of all that for us.\n\n## Protecting routes in the `Item` resource\n\n```python title=\"resources/item.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n# highlight-start\nfrom flask_jwt_extended import jwt_required\n# highlight-end\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    # highlight-start\n    @jwt_required()\n    # highlight-end\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    # highlight-start\n    @jwt_required()\n    # highlight-end\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(**item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    # highlight-start\n    @jwt_required()\n    # highlight-end\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    # highlight-start\n    @jwt_required()\n    # highlight-end\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n```\n\n## Error handling with Flask-JWT-Extended\n\nThere are many things that could go wrong with JWTs:\n\n- The JWT may be expired (they don't last forever!)\n- The JWT may be invalid, such as if the client makes changes to it\n- A JWT may be required, but none was provided\n- There's more (we'll look at them in coming lectures!)\n\nLet's go to `app.py` and add some configuration to tell Flask-JWT-Extended what to do in each of these cases.\n\nAt the top, let's import `jsonify`:\n\n```python title=\"app.py\"\nfrom flask import Flask, jsonify\n```\n\nThen, after we define the `jwt = JWTManager(app)` variable, we can write some functions, each of which can run in different problem scenarios.\n\n```python title=\"app.py\"\n...\n\napp.config[\"JWT_SECRET_KEY\"] = \"jose\"\njwt = JWTManager(app)\n\n@jwt.expired_token_loader\ndef expired_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n        401,\n    )\n\n@jwt.invalid_token_loader\ndef invalid_token_callback(error):\n    return (\n        jsonify(\n            {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n        ),\n        401,\n    )\n\n@jwt.unauthorized_loader\ndef missing_token_callback(error):\n    return (\n        jsonify(\n            {\n                \"description\": \"Request does not contain an access token.\",\n                \"error\": \"authorization_required\",\n            }\n        ),\n        401,\n    )\n\n...\n```\n\n:::tip\nNote that some Flask-JWT-Extended error functions take two arguments: `jwt_header` and `jwt_payload`. Others take a single argument, `error`.\n\nThe ones that don't take JWT information are those that would be called when a JWT is not present (above, when the JWT is invalid or required but not received).\n:::"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\n        \"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/README.md",
    "content": "---\ntitle: JWT claims and authorization\ndescription: Learn how to add claims (extra info) to a JWT and use it for authorization in endpoints of a REST API.\nctslug: jwt-claims-and-authorization\n---\n\n# JWT Claims and Authorization\n\nJWT claims are extra data we can add to the JWT. For example, we could store in the JWT whether the user whose ID is stored in the JWT is an \"administrator\" or not.\n\nBy doing this, we only have to check the user's permissions once, when we create the JWT, and not every time the user makes a request.\n\nTo add a custom claim to a JWT we define a function similar to the error handling functions we wrote in the last lecture:\n\n```python title=\"app.py\"\napp.config[\"JWT_SECRET_KEY\"] = \"jose\"\njwt = JWTManager(app)\n\n# highlight-start\n@jwt.additional_claims_loader\ndef add_claims_to_jwt(identity):\n    if identity == 1:\n        return {\"is_admin\": True}\n    return {\"is_admin\": False}\n# highlight-end\n\n@jwt.expired_token_loader\ndef expired_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n        401,\n    )\n```\n\n:::caution Read from a database or config file\nHere we're assuming that the user with and ID of `1` will be the administrator. Normally you'd read this from either a config file or the database.\n:::\n\n## How to use JWT claims in an endpoint\n\nLet's make a small change to the `Item` resource so that only admins can delete items.\n\nTo do so, we'll need to add an import for `get_jwt`:\n\n```python title=\"resources/item.py\"\nfrom flask_jwt_extended import jwt_required, get_jwt\n```\n\nThen in the `delete` endpoint, we can use `get_jwt()` to check the data in the JWT (which behaves like a dictionary):\n\n```python title=\"resources/item.py\"\n@jwt_required()\ndef delete(self, item_id):\n    # highlight-start\n    jwt = get_jwt()\n    if not jwt.get(\"is_admin\"):\n        abort(401, message=\"Admin privilege required.\")\n    # highlight-end\n\n    item = ItemModel.query.get_or_404(item_id)\n    db.session.delete(item)\n    db.session.commit()\n    return {\"message\": \"Item deleted.\"}\n```"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n\n\n@pytest.fixture()\ndef admin_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(\n            identity=1, additional_claims={\"is_admin\": True}\n        )\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, admin_jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {admin_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_delete_item_without_admin(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Admin privilege required.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/README.md",
    "content": "---\ntitle: How to add logout to the REST API\ndescription: Create a logout endpoint that blocks certain JWTs from making further authenticated requests.\nctslug: how-to-add-logout-to-the-rest-api\n---\n\n# How to add logout to the REST API\n\nTo log an user out we must _revoke_ their JWT. That way, if they send us the same JWT again, we can check whether it's been revoked or not. If it has, then we won't authorize them.\n\nTo do this, we need a central store of revoked JWTs that we keep around at least until the revoked JWT has expired.\n\nLet's create our central revoked JWT storage in a file called `blocklist.py`. You could store this in the database instead, if you prefer. I'll leave that as an exercise for you.\n\n```python title=\"blocklist.py\"\n\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n```\n\n## Flask-JWT-Extended blocklist configuration for user logout\n\nNow, in `app.py`, let's add some more Flask-JWT-Extended configuration to do two things:\n\n- Check whether any JWT received is in the blocklist.\n- If they are, return an error message to that effect.\n\n```python title=\"app.py\"\nfrom blocklist import BLOCKLIST\n\n...\n\n@jwt.token_in_blocklist_loader\ndef check_if_token_in_blocklist(jwt_header, jwt_payload):\n    return jwt_payload[\"jti\"] in BLOCKLIST\n\n\n@jwt.revoked_token_loader\ndef revoked_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n        ),\n        401,\n    )\n```\n\n## How to perform logout (i.e. add JWTs to the blocklist)\n\nFinally we need a resource in `resources/user.py` to actually add the user's JWT to the blocklist when they log out.\n\n```python title=\"resources/user.py\"\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    # highlight-start\n    get_jwt,\n    jwt_required,\n    # highlight-end\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n# highlight-start\nfrom blocklist import BLOCKLIST\n# highlight-end\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n# highlight-start\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n# highlight-end\n\n\n# Other User routes here\n```"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n\n\n@pytest.fixture()\ndef admin_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(\n            identity=1, additional_claims={\"is_admin\": True}\n        )\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, admin_jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {admin_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_delete_item_without_admin(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Admin privilege required.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_logout_user(client, created_user_jwt):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Successfully logged out\"\n\n\ndef test_logout_user_twice(client, created_user_jwt):\n    client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"description\": \"The token has been revoked.\",\n        \"error\": \"token_revoked\",\n    }\n\n\ndef test_logout_user_no_token(client):\n    response = client.post(\n        \"/logout\",\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"description\"] == \"Request does not contain an access token.\"\n\n\ndef test_logout_user_invalid_token(client):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": \"Bearer bad_token\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"error\": \"invalid_token\",\n        \"message\": \"Signature verification failed.\",\n    }\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n\n\n@pytest.fixture()\ndef admin_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(\n            identity=1, additional_claims={\"is_admin\": True}\n        )\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, admin_jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {admin_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_delete_item_without_admin(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Admin privilege required.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import create_access_token\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/10_logout_users_rest_api/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/11_insomnia_request_chaining/README.md",
    "content": "---\ntitle: Insomnia request chaining\ndescription: \"Learn how to use Insomnia's Request Chaining to simplify our workflow and not have to copy-paste the access token in every request.\"\nctslug: request-chaining-with-insomnia\n---\n\n# Request chaining with Insomnia\n\nUp until now, we've been able to log in, get an access token, and then use that acces token to access the protected endpoints in our API. However, this workflow is a bit tedious, and we have to copy-paste the access token into every request. Luckily, Insomnia has a feature called [request chaining](https://support.insomnia.rest/article/26-request-chaining) that allows us to simplify this workflow!\n\n## How to use request chaining with Insomnia\n\nInstead of passing the JWT in every request, you can access the `Headers`  section and follow these steps. In the `Authorization` field, type `Bearer` add a space and then press `CTRL + SPACE` to get a contextual menu. Navigate to the `Response -> Body Attribute` field and select it, as shown in the screenshot below:\n\n![Contextual menu with Generator Tag options](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/bearer_token_qk2jqi.png)\n\nAfter you've selected it, you'll see an error that looks like this:\n\n![Response error in the Authorization field](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/error_darkak.png)\n\nIf you click on the error, you'll see a modal window:\n\n![Modal window with empty fields](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/modal_xutepw.png)\n\nInside of this window, you will first need to select a request that you want to get the data from. Click on `Select item` inside the `Request` field and choose the `POST /login` endpoint. After you've done that, go to the `Filter` field. In this field, we are telling Insomnia which part of the response we want to get from it. The prompt is started with the dollar sign `$` and then we can use the dot to access the attributes of the response. If we only type `$` or `$.`, we'll get a list of all the attributes of the response:\n\n![Attributes recieved from request using a dollar sign](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/attributes_kxngsi.png)\n\nIn our case, we want to get the `access_token` attribute from the response, so we type `$.access_token`. If there was something else inside the `access_token` that you wanted to access like some value for example, you could do `$.access_token.value` and so on. After you've added the filter, you can set the `Trigger Behavior`. For example, you can set it to `When expired` and set time to 300 seconds. This means that the token will be refreshed every 5 minutes. The filled out window should look like this:\n\n![Filled out modal window with attributes recieved from request using a dollar sign followed by .access_token](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/filled_out_muzo2u.png)\n\nYou can now press `Done` and you'll see that the error is gone and that the `Authorization` field is filled out with the `Bearer` token:\n\n![Filled out Authorization field with Bearer token](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/token_txc8sb.png)\n\nSince we use multiple protected endpoints, we would need to use the same method to get the `access_token` for every request. To tackle this problem, we can use [environment variables](https://support.insomnia.rest/article/13-environment-variables), as we did before for the `url` variable.\n\nTo do this, you need to create a new environment variable. Click on the `No Environment` button in the top left corner of the Insomnia window and then click on the `Manage Environments` button, you will see a base environment that looks like this:\n\n![Base environment with url environment variable only](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/base_environment_cfyrab.png)\n\nAdd a new environment variable called `access_token`, and for its value you should follow the same process from above. As a quick reminder, you should press `CTRL + SPACE` to get the contextual menu and then select the `Response -> Body Attribute` field. Click on the error that will show up and fill out the modal window as we've done before. Make sure to wrap the `Response -> Body Attribute` with quotation marks, as shown in the screenshot. When the variable is created, your environment should look like this:\n\n![Base environment with added access_token variable](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/environment_n8jfm9.png)\n\nYou will now be able to use this environment variable in all of the endpoints by simply writing `{{access_token}}` in the field that you want to use it in. Coming back to the first example, you can use the access token in the `Authorization` field by writing `Bearer {{access_token}}` and this will be the result:\n\n![Bearer with access_token environment variable](https://res.cloudinary.com/teclado/image/upload/v1689180763/courses/rest-apis-flask-python/result_krbno3.png)\n\nIf you try and make the request, you will see that it works just like before, without needing to copy and paste the access token.\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/README.md",
    "content": "---\ntitle: Token refreshing with Flask-JWT-Extended\ndescription: Learn about fresh and non-fresh tokens, as well as how to use a refresh token to generate a new, non-fresh access token.\nctslug: token-refreshing-with-flask-jwt-extended\n---\n\n# Token refreshing with Flask-JWT-Extended\n\nOne of the problems with JWT authentication is that JWTs expire, and then the user has to re-authenticate by providing their username and password.\n\nHow long to set the JWT expiry time is tricky. If it's very long, it's more likely that a different person may use the same device to access the website, and the previous user's account will still be logged in. If it's very short though, it's really annoying for users.\n\nThis is where the concept of **token refreshing** comes into play.\n\nWe can provide our users two tokens: an **access token** that they can use to, well, access endpoints, and a **refresh token** that they can use to get a new access token without having to provide their username and password.\n\n:::tip When do clients use the refresh token?\nWhen a client makes a request and sends the access token, if the token has expired our API sends back a message to that effect.\n\nAt that point, the client can then, behind the scenes and without the user noticing, use the refresh to get a new access token, and re-request the original page.\n\nFor a client, the authentication flow is a three-step process:\n\n1. Send the access token they've got stored (may or may not be fresh).\n2. If API responds with a 401 Unauthorized, use the refresh token to get a new access token and try again. Now you've got a new, non-fresh access token.\n3. If the API responds with another 401 Unauthorized, ask the user to log in again. Now you've got a fresh access token.\n:::\n\nThe important thing here is **token freshness**. \n\n- A **fresh access token** is given to users immediately after logging in.\n- A **non-fresh access token** is given to users when they use their refresh token.\n\nThis is important, because it means that we can protect certain routes by requiring a fresh access token. Since these tokens are only generated in response to login, we know that the user is probably who they say they are, and they haven't simply forgotten to log out.\n\nAs an example, if the user goes to their \"delete my account\" page, we might want a fresh token to access that endpoint. However, if they're simply going to their profile page, we may accept a non-fresh token.\n\n## How to create refresh tokens with Flask-JWT-Extended\n\nWhen a user logs in, we can create the access token and the refresh token at the same time. We will also make sure that the access token is marked as **fresh**.\n\nFirst, let's add new imports:\n\n```diff title=\"resources/user.py\"\nfrom flask_jwt_extended import (\n    create_access_token,\n+   create_refresh_token,\n+   get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\n```\n\nThen let's change our `UserLogin` route:\n\n```python title=\"resources/user.py\"\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            # highlight-start\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n            # highlight-end\n\n        abort(401, message=\"Invalid credentials.\")\n```\n\n**Update Nov 2024**: Before now, we used `identity=user.id`, but now we have to convert it to a string first.\n\n## Writing the token refresh endpoint\n\nWhen a user logs in, they will now have the access token and the refresh token.\n\nLet's code another endpoint that will take the refresh token and return a new, non-fresh access token:\n\n```python title=\"resources/user.py\"\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n```\n\n**Note**: here we don't need to call `str(current_user)` because `get_jwt_identity()` returns what we previously stored, which is a string.\n\nNote that above, we've told Flask-JWT-Extended that a refresh token is required with `@jwt_required(refresh=True)`. We'll do something similar for requiring fresh tokens in a second!\n\n## Requiring a fresh token for certain endpoints\n\nLet's go to the create item endpoint and mark it as needing a fresh token. Normally, fresh tokens would be required for destructive operations such as changing passwords or deleting accounts.\n\n```python title=\"resources/item.py\"\n# highlight-start\n@jwt_required(fresh=True)\n# highlight-end\n@blp.arguments(ItemSchema)\n@blp.response(201, ItemSchema)\ndef post(self, item_data):\n    item = ItemModel(**item_data)\n\n    try:\n        db.session.add(item)\n        db.session.commit()\n    except SQLAlchemyError:\n        abort(500, message=\"An error occurred while inserting the item.\")\n\n    return item\n```\n\n## Error handling when a fresh token is required\n\nWhen a fresh token is required but a non-fresh token is provided, we want the Flask app to return a message to that effect. We can do this just as we did with the other Flask-JWT-Extended configurations:\n\n```python title=\"app.py\"\n@jwt.needs_fresh_token_loader\ndef token_not_fresh_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\n                \"description\": \"The token is not fresh.\",\n                \"error\": \"fresh_token_required\",\n            }\n        ),\n        401,\n    )\n```"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef fresh_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1, fresh=True)\n        return access_token\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n\n\n@pytest.fixture()\ndef admin_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(\n            identity=1, additional_claims={\"is_admin\": True}\n        )\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, fresh_jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, fresh_jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, fresh_jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, fresh_jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_create_item_with_non_fresh_jwt(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"description\": \"The token is not fresh.\",\n        \"error\": \"fresh_token_required\",\n    }\n\n\ndef test_delete_item(client, admin_jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {admin_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_delete_item_without_admin(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Admin privilege required.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, fresh_jwt, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, fresh_jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, fresh_jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, fresh_jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {fresh_jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwts(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"], response.json[\"refresh_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_logout_user(client, created_user_jwts):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[0]}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Successfully logged out\"\n\n\ndef test_logout_user_twice(client, created_user_jwts):\n    client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[0]}\"},\n    )\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[0]}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"description\": \"The token has been revoked.\",\n        \"error\": \"token_revoked\",\n    }\n\n\ndef test_logout_user_no_token(client):\n    response = client.post(\n        \"/logout\",\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"description\"] == \"Request does not contain an access token.\"\n\n\ndef test_logout_user_invalid_token(client):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": \"Bearer bad_token\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"error\": \"invalid_token\",\n        \"message\": \"Signature verification failed.\",\n    }\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_refresh_token_invalid(client):\n    response = client.post(\n        \"/refresh\",\n        headers={\"Authorization\": \"Bearer bad_jwt\"},\n    )\n\n    assert response.status_code == 401\n\n\ndef test_refresh_token(client, created_user_jwts):\n    response = client.post(\n        \"/refresh\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[1]}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_refresh_token_twice(client, created_user_jwts):\n    client.post(\n        \"/refresh\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[1]}\"},\n    )\n    response = client.post(\n        \"/refresh\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwts[1]}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"description\": \"The token has been revoked.\",\n        \"error\": \"token_revoked\",\n    }\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/end_video/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/.flake8",
    "content": "[flake8]\nper-file-ignores = __init__.py:F401"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/conftest.py",
    "content": "import pytest\nfrom flask_jwt_extended import create_access_token\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n\n\n@pytest.fixture()\ndef jwt(app):\n    with app.app_context():\n        access_token = create_access_token(identity=1)\n        return access_token\n\n\n@pytest.fixture()\ndef admin_jwt(app):\n    with app.app_context():\n        access_token = create_access_token(\n            identity=1, additional_claims={\"is_admin\": True}\n        )\n        return access_token\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/requirements-dev.txt",
    "content": "pytest\nblack\nflake8"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, jwt, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client, jwt):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, admin_jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {admin_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_delete_item_without_admin(client, jwt, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Admin privilege required.\"\n\n\ndef test_update_item(client, jwt, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client, jwt):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client, jwt):\n    response = client.get(\n        \"/item\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, jwt, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client, jwt):\n    response = client.get(\n        \"/item/1\",\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, jwt, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id, jwt):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client, jwt):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n        headers={\"Authorization\": f\"Bearer {jwt}\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/__tests__/test_user.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_user_details(client):\n    username = \"test_user\"\n    password = \"test_password\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return username, password\n\n\n@pytest.fixture()\ndef created_user_jwt(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    return response.json[\"access_token\"]\n\n\ndef test_register_user(client):\n    username = \"test_user\"\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json == {\"message\": \"User created successfully.\"}\n\n\ndef test_register_user_already_exists(client):\n    username = \"test_user\"\n    client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    response = client.post(\n        \"/register\",\n        json={\"username\": username, \"password\": \"Test Password\"},\n    )\n\n    assert response.status_code == 409\n    assert response.json[\"message\"] == \"A user with that username already exists.\"\n\n\ndef test_register_user_missing_data(client):\n    response = client.post(\n        \"/register\",\n        json={},\n    )\n\n    assert response.status_code == 422\n    assert \"password\" in response.json[\"errors\"][\"json\"]\n    assert \"username\" in response.json[\"errors\"][\"json\"]\n\n\ndef test_login_user(client, created_user_details):\n    username, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": password},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"access_token\"]\n\n\ndef test_login_user_bad_password(client, created_user_details):\n    username, _ = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": username, \"password\": \"bad_password\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_login_user_bad_username(client, created_user_details):\n    _, password = created_user_details\n    response = client.post(\n        \"/login\",\n        json={\"username\": \"bad_username\", \"password\": password},\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"message\"] == \"Invalid credentials.\"\n\n\ndef test_logout_user(client, created_user_jwt):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Successfully logged out\"\n\n\ndef test_logout_user_twice(client, created_user_jwt):\n    client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": f\"Bearer {created_user_jwt}\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"description\": \"The token has been revoked.\",\n        \"error\": \"token_revoked\",\n    }\n\n\ndef test_logout_user_no_token(client):\n    response = client.post(\n        \"/logout\",\n    )\n\n    assert response.status_code == 401\n    assert response.json[\"description\"] == \"Request does not contain an access token.\"\n\n\ndef test_logout_user_invalid_token(client):\n    response = client.post(\n        \"/logout\",\n        headers={\"Authorization\": \"Bearer bad_token\"},\n    )\n\n    assert response.status_code == 401\n    assert response.json == {\n        \"error\": \"invalid_token\",\n        \"message\": \"Signature verification failed.\",\n    }\n\n\ndef test_get_user_details(client, created_user_details):\n    response = client.get(\n        \"/user/1\",  # assume user id is 1\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"username\": created_user_details[0],\n    }\n\n\ndef test_get_user_details_missing(client):\n    response = client.get(\n        \"/user/23\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required()\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id))\n            return {\"access_token\": access_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/12_token_refreshing_flask_jwt_extended/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/Insomnia_section8_before_chaining.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:31:21.063Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_adb596c3d0ee48e2b009a555297f36ac\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667331428779,\"created\":1667328670383,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1667328596182,\"created\":1667328596182,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"parentId\":null,\"modified\":1666991842388,\"created\":1666991842388,\"name\":\"Section 8\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_fe639dc55a2d439f9aa7a6bceee6a9fa\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332404117,\"created\":1667328597818,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5e7ca3df73314f8c839493d53528760b\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332406439,\"created\":1667331560707,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_bc66397203734df4b4eb6d153d8d3ec3\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1668007854634,\"created\":1667332459073,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_953d7fdcaf1f433b9b3b0cdf6453b0cd\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332353929,\"created\":1667328704142,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_362ff303b1054bd0b0d62522803aea64\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842419,\"created\":1666991842419,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842417,\"created\":1666991842417,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_f613fc18d27648daa00d6b78deea5b66\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842418,\"created\":1666991842418,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e899763b1c0a46ad9eb47f9e628aa643\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842422,\"created\":1666991842422,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_266cac65472a4a118929460e58893fb0\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842421,\"created\":1666991842421,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e63e0532c2c340aa8ed6643f0ae1b4ec\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842424,\"created\":1666991842424,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5c6887cfb9c94dc2aa2fe76d525fecac\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842426,\"created\":1666991842426,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_2ba32c3564f3456aa1c8731323508968\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1666991842400,\"created\":1666991842400,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842399,\"created\":1666991842399,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"req_047bf72b98214de4a8f711fd46b73eb4\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1666991842403,\"created\":1666991842403,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_57daa411077044a98d3b2534ee735703\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1667332324728,\"created\":1666991842402,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6c1af0972dbe4faebf5973d95670c241\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1667332341585,\"created\":1666991842405,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0d1c09067c6a419a9096b32da16d01a3\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332272845,\"created\":1666991842412,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842407,\"created\":1666991842407,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_dbdfc5218bf54bfab39db152b3ffe982\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332288041,\"created\":1666991842414,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_32c5d69aec8f44fdb33852b456c99b7a\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332259971,\"created\":1666991842408,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1843f2db017842b993251abbedeb2e8b\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332298429,\"created\":1666991842410,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d311f219368d46a6b6d92be142a32763\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332311745,\"created\":1666991842413,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_52ad539864c4425884f6394f62627cb7\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1667329454452,\"created\":1666991842391,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_bc7c508b3eed4f2485782e9d7177762e\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842395,\"created\":1666991842395,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_75bd139c1688445faff87b21105f8df1\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842435,\"created\":1666991842397,\"fileName\":\"Section 8\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/Insomnia_section8_chaining.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-09T15:31:42.216Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_00cda5fbc60a4905bf5b8e67aa7dadb4\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566874,\"created\":1667332566874,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566872,\"created\":1667332566872,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"parentId\":null,\"modified\":1667332566837,\"created\":1667332566837,\"name\":\"Section 8 - Chaining\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_0ae766df1c25481e83bd6ade061a919b\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667676377948,\"created\":1667650753174,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_252ddd492b9242ffb1a6fe21e25534c5\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667650976611,\"created\":1667332566872,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fe8e836ebd4a44cd9f37ff8738e7ed98\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667650979775,\"created\":1667332566877,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6a638b0dfd3040118a0282fe5a49b5fb\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566878,\"created\":1667332566878,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_229bc7e2601e44cb82eb3e5eafa90202\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566875,\"created\":1667332566875,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ba56440f8f204525a13fbdfbd2273ae0\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566864,\"created\":1667332566864,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566861,\"created\":1667332566861,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_e2bc3c7dd1c240baad60b3881359bf38\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566862,\"created\":1667332566862,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ae7151238fe84a65b5b1064b87cef5bd\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566867,\"created\":1667332566867,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9237adf4f3e74e1b9aa8dbb15680c2f3\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566866,\"created\":1667332566866,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_c11ab9d081754c3e95f0eed61219454a\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566868,\"created\":1667332566868,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_03468f66652a42dcbd34ef05ce4fd714\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566870,\"created\":1667332566870,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_22eb90da9d974ac2880b9207d6d11c01\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566845,\"created\":1667332566845,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_6315121566934d098d5f963bb2b25679\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566844,\"created\":1667332566844,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"req_44b8b847e3bd4b35bb7c5d2df5e0ee94\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566849,\"created\":1667332566849,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_3efb83da0cb34711961a59b6b3b39278\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566848,\"created\":1667332566848,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_24669eae6530416094eb2c746ae577ed\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566851,\"created\":1667332566851,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_41e02aac89e5447fb70b42a5fbb6e42d\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667650833785,\"created\":1667332566857,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_68ade160b31f446995815fec3dc30a93\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566853,\"created\":1667332566853,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_51ea1a9b34584746a8368ede6f6bfadf\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332848385,\"created\":1667332566859,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_69cc615e910a4ca7a91382b455c3ceb5\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332840222,\"created\":1667332566854,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_310092462fe44643bdbcf671dd488033\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332838232,\"created\":1667332566855,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_260395e8e71c47559f2c70f50618ea38\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332566858,\"created\":1667332566858,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_c2a4191124a544e9a440f8042644c7b9\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667651225760,\"created\":1667332566839,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_8a4500f913d7401fa00168eaf32dcb4c\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566841,\"created\":1667332566841,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_2d4dfbfd115b4f91b44d09ad3c6d0b5b\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566888,\"created\":1667332566842,\"fileName\":\"Section 8 - Chaining\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/08_flask_jwt_extended/_category_.json",
    "content": "{\n    \"label\": \"User Authentication with Flask-JWT-Extended\",\n    \"position\": 8\n}\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/01_why_use_database_migrations/README.md",
    "content": "---\ntitle: Why use database migrations?\ndescription: Learn about database migrations and what they are useful for.\nctslug: why-use-database-migrations\n---\n\n# Why use database migrations?\n\nAs you work on your application, particularly over a long time, it is unavoidable that you will want to add columns to your models, or even add new models entirely.\n\nMaking the changes directly to the models without something like Alembic and Flask-Migrate will mean that the existing database tables and the model definitions will be out of sync. When that happens, SQLAlchemy usually complains and your application won't work.\n\nAn option is to delete everything and get SQLAlchemy to re-create the tables. Obviously, this is not good if you have data in the database as you would lose all the data.\n\nWe can use Alembic to detect the changes to the models, and what steps are necessary to \"upgrade\" the database so it matches the new models. Then we can use Alembic to actually modify the database following the upgrade steps.\n\nAlembic also tracks each of these migrations over time, so that you can easily go to a past version of the database. This is useful if bugs are introduced or the feature requirements change.\n\nSince Alembic tracks all the migrations over time, we can use it to create the tables from scratch, simply by applying the migrations one at a time until we reach the latest one (which should be equal to the current one)."
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/README.md",
    "content": "---\ntitle: How to add Flask-Migrate to our Flask app\ndescription: Integrating your Flask app with Flask-Migrate is relatively straightforward. Learn how to do it in this lecture.\nctslug: how-to-add-flask-migrate-to-our-flask-app\n---\n\n# How to add Flask-Migrate to our Flask app\n\nAdding Flask-Migrate to our app is simple, just install it and add a couple lines to `app.py`.\n\nTo install:\n\n```bash\npip install flask-migrate\n```\n\nThis will also install Alembic, since it is a dependency.\n\nThen we need to add 2 lines to `app.py` (highlighted):\n\n```py\nfrom flask_smorest import Api\n# highlight-start\nfrom flask_migrate import Migrate\n# highlight-end\n\nimport models\n\napp.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\ndb.init_app(app)\n# highlight-start\nmigrate = Migrate(app, db)\n# highlight-end\napi = Api(app)\n\nwith app.app_context():\n    db.create_all()\n```\n\nSince we will be using Flask-Migrate to create our database, we no longer need to tell Flask-SQLAlchemy to do it when we create the app.\n\nDelete these two lines:\n\n```py\nwith app.app_context():\n    db.create_all()\n```\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn\nFlask-Migrate"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n    # JWT configuration ends\n\n    with app.app_context():\n        import models  # noqa: F401\n\n        db.create_all()\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/02_add_flask_migrate_to_app/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/README.md",
    "content": "---\ntitle: Initialize your database with Flask-Migrate\ndescription: \"Learn the first steps when starting with Flask-Migrate: initializing the database.\"\nctslug: initialize-database-with-flask-migrate\n---\n\n# Initialize the database with Flask-Migrate\n\nActivate your virtual environment and run this command:\n\n```\nflask db init\n```\n\nThis will create a `migrations` folder inside your project folder.\n\nIn the `migrations` folder you'll find a few things:\n\n- The `versions` folder is where migration scripts will be placed. These will be used by Alembic to make changes to the database.\n- `alembic.ini` is the Alembic configuration file.\n- `env.py` is a script used by Alembic to generate migration files.\n- `script.py.mako` is the template file for migration files.\n\n## Generate the first migration to set up the database\n\nNow that we're set up, we need to make sure that the database we want to use is currently empty. In our case, since we're using SQLite, just delete `data.db`.\n\nThen, run this command:\n\n```\nflask db migrate\n```\n\nThis will create the migration file.\n\n\n:::caution\nIt's important to double-check the migration script and make sure it is correct! Compare it with your model definitions and make sure nothing is missing.\n:::\n\nNow let's actually apply the migration:\n\n```\nflask db upgrade\n```\n\nThis will create the `data.db` file. If you were using another RDBMS (like PostgreSQL or MySQL), this command would create the tables using the existing model definitions.\n\n:::info How does the database know which version it's on?\nWhen using Alembic to create the database tables from scratch, it creates an extra table with a single row, that stores the current migration version.\n\nYou'll note in each migration script there is information about the previous migration and the next migration.\n\nThis is why it's important to **never rename the migration files or change the revision identifiers at the top of those files**.\n:::"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/.python-version",
    "content": "3.11\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n\ndef get_engine():\n    try:\n        # this works with Flask-SQLAlchemy<3 and Alchemical\n        return current_app.extensions['migrate'].db.get_engine()\n    except TypeError:\n        # this works with Flask-SQLAlchemy>=3\n        return current_app.extensions['migrate'].db.engine\n\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url', str(get_engine().url).replace('%', '%%'))\ntarget_db = current_app.extensions['migrate'].db\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef get_metadata():\n    if hasattr(target_db, 'metadatas'):\n        return target_db.metadatas[None]\n    return target_db.metadata\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=get_metadata(), literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=get_metadata(),\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/migrations/versions/c575166f6192_.py",
    "content": "\"\"\"empty message\n\nRevision ID: c575166f6192\nRevises: \nCreate Date: 2023-01-23 15:14:42.094596\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'c575166f6192'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn\nFlask-Migrate"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn\nFlask-Migrate"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/03_initialize_database_flask_db_init/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/README.md",
    "content": "---\ntitle: Change SQLAlchemy models and generate a migration\ndescription: Use Flask-Migrate to generate a new database migration after changing your SQLAlchemy models.\nctslug: change-sqlalchemy-models-generate-migration\n---\n\n# Change SQLAlchemy models and generate a migration\n\nLet's make a change to one of our SQLAlchemy models and then generate another migration script. This is what we will do every time we want to make changes to our models and our database schema.\n\n```python title=\"models/item.py\"\nfrom db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    # highlight-start\n    description = db.Column(db.String)\n    # highlight-end\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n```\n\nHere we're adding a simple column, just a string that doesn't have any constraints.\n\nNow let's go to the terminal and run the command:\n\n```\nflask db migrate\n```\n\nThis will now generate _another migration script_ that you have to double-check. Make sure to check the upgrade and downgrade functions.\n\nWhen you're happy with the contents, apply the migration:\n\n```\nflask db upgrade\n```"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/.python-version",
    "content": "3.11\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n\ndef get_engine():\n    try:\n        # this works with Flask-SQLAlchemy<3 and Alchemical\n        return current_app.extensions['migrate'].db.get_engine()\n    except TypeError:\n        # this works with Flask-SQLAlchemy>=3\n        return current_app.extensions['migrate'].db.engine\n\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url', str(get_engine().url).replace('%', '%%'))\ntarget_db = current_app.extensions['migrate'].db\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef get_metadata():\n    if hasattr(target_db, 'metadatas'):\n        return target_db.metadatas[None]\n    return target_db.metadata\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=get_metadata(), literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=get_metadata(),\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/versions/bcc005bc255c_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bcc005bc255c\nRevises: c575166f6192\nCreate Date: 2023-01-23 15:21:21.002304\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bcc005bc255c'\ndown_revision = 'c575166f6192'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    with op.batch_alter_table('items', schema=None) as batch_op:\n        batch_op.add_column(sa.Column('description', sa.String(), nullable=True))\n\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    with op.batch_alter_table('items', schema=None) as batch_op:\n        batch_op.drop_column('description')\n\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/migrations/versions/c575166f6192_.py",
    "content": "\"\"\"empty message\n\nRevision ID: c575166f6192\nRevises: \nCreate Date: 2023-01-23 15:14:42.094596\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'c575166f6192'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn\nFlask-Migrate"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/.dockerignore",
    "content": ".venv\n*.pyc\n__pycache__\ndata.db"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/.python-version",
    "content": "3.11\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    # @jwt.additional_claims_loader\n    # def add_claims_to_jwt(identity):\n    #     # TODO: Read from a config file instead of hard-coding\n    #     if identity == 1:\n    #         return {\"is_admin\": True}\n    #     return {\"is_admin\": False}\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n\n\n    api.register_blueprint(UserBlueprint)\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n\ndef get_engine():\n    try:\n        # this works with Flask-SQLAlchemy<3 and Alchemical\n        return current_app.extensions['migrate'].db.get_engine()\n    except TypeError:\n        # this works with Flask-SQLAlchemy>=3\n        return current_app.extensions['migrate'].db.engine\n\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url', str(get_engine().url).replace('%', '%%'))\ntarget_db = current_app.extensions['migrate'].db\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef get_metadata():\n    if hasattr(target_db, 'metadatas'):\n        return target_db.metadatas[None]\n    return target_db.metadata\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=get_metadata(), literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=get_metadata(),\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/migrations/versions/c575166f6192_.py",
    "content": "\"\"\"empty message\n\nRevision ID: c575166f6192\nRevises: \nCreate Date: 2023-01-23 15:14:42.094596\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'c575166f6192'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn\nFlask-Migrate"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/resources/__init__.py",
    "content": ""
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom db import db\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        # Make it clear that when to add the refresh token to the blocklist will depend on the app design\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/04_change_models_generate_alembic_migration/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n"
  },
  {
    "path": "docs/docs/09_flask_migrate/05_manually_review_modify_migrations/README.md",
    "content": "---\ntitle: Manually review and modify database migrations\ndescription: Alembic can generate database migrations parting from model changes, but sometimes we need to modify them manually.\nctslug: manually-review-modify-database-migrations\n---\n\n# Manually review and modify database migrations\n\n## Default column values\n\nWhen you add a column that uses a default value, any rows that existed previously will have `null` as the value.\n\nYou'll have to go into the database to add the default value to those rows.\n\nYou can also do this during the migration, since Alembic migrations can execute any arbitrary SQL queries.\n\nHere's an example for a column being added with a default value:\n\n```py title=\"migrations/versions/sample_migration.py\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"9c386e4052be\"\ndown_revision = \"713af8a4cb34\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\n        \"invoices\",\n        sa.Column(\"enable_downloads\", sa.Boolean(), nullable=True, default=False),\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column(\"invoices\", \"enable_downloads\")\n    # ### end Alembic commands ###\n```\n\nYou can see that we're adding a column called `enable_downloads` to the `invoices` table. The default value for new rows will be `False`, but what is the value for all the invoices we already have in the database?\n\nThe value will be undefined, or `null`.\n\nWhat we must do is tell Alembic to insert `False` into each of the existing rows. We can do that with SQL:\n\n```py title=\"migrations/versions/sample_migration.py\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"9c386e4052be\"\ndown_revision = \"713af8a4cb34\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\n        \"invoices\",\n        sa.Column(\"enable_downloads\", sa.Boolean(), nullable=True, default=False),\n    )\n    # highlight-start\n    op.execute(\"UPDATE invoices SET enable_downloads = False\")\n    # highlight-end\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column(\"invoices\", \"enable_downloads\")\n    # ### end Alembic commands ###\n```"
  },
  {
    "path": "docs/docs/09_flask_migrate/_category_.json",
    "content": "{\n    \"label\": \"Database migrations with Alembic and Flask-Migrate\",\n    \"position\": 9\n}\n"
  },
  {
    "path": "docs/docs/10_git_crash_course/README.md",
    "content": "# Git Crash Course\n\nThe Git Crash Course e-book is hosted in a different page because it is used in multiple courses.\n\nRead the Git Crash Course e-book here: [https://git-workshop.tecladocode.com/](https://git-workshop.tecladocode.com/)"
  },
  {
    "path": "docs/docs/10_git_crash_course/_category_.json",
    "content": "{\n    \"label\": \"Git Crash Course\",\n    \"position\": 10\n}\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/01_section_overview/README.md",
    "content": "---\nctslug: overview-of-this-section\n---\n\n# Overview of this section\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\nIn this section, we will figure out how to get our Flask app and put it on a public server so other people can interact with it! This is called \"deploying\".\n\nThere are many services we can use to deploy our app. Most of them have some sort of \"free tier\" so that you can try the deployment without having to pay anything. Usually, if you want better performance or unlimited usage, you have to pay.\n\nRemember that just as we run the Flask app in our computers, when we deploy it the app runs in a server, somewhere in the world. For all intents and purposes, the server is just like our computer!\n\nServers usually run Linux, so we can deploy our Docker images without a performance hit as we would using Mac or Windows.\n\nAt the end of the section, you'll be able to access your API using a URL such as [https://rest-api-smorest-docker.onrender.com](https://rest-api-smorest-docker.onrender.com).\n\nFor this section, our deployment will be completely free. We will deploy our Flask app for free, and we will also get a free PostgreSQL database on the cloud using ElephantSQL.\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/02_create_render_web_service/README.md",
    "content": "---\nctslug: creating-a-render-com-web-service\n---\n\n# Creating a Render.com web service\n\nLet's start by going to [https://render.com](https://render.com) and signing up to an account. You can \"Log in with GitHub\" to make things easier.\n\nOnce you've logged in, you'll see in your [Dashboard](https://dashboard.render.com/services) that you can create a new service using a button at the top right of the page.\n\nClick it, and select \"Web Service\".\n\nOptions other than \"Web Service\" are useful for different kinds of applications, and some are databases that you can use (but not for free, so we won't use Render for our database in this section).\n\nThen you'll [connect your GitHub account](https://render.com/docs/github) if you haven't already, and look for your repositories.\n\nSelect the repository that you created during this course:\n\n![Render.com screenshot showing how to search for and select a repository to connect to from GitHub](https://res.cloudinary.com/teclado/image/upload/v1689180776/courses/rest-apis-flask-python/render-github-connect_jp6mip.png)\n\nThen, give it a name and make sure the configuration is as follows:\n\n![Render.com screenshot showing the web service configuration](https://res.cloudinary.com/teclado/image/upload/v1689180776/courses/rest-apis-flask-python/render-service-config_poweeb.png)\n\n- Make sure \"Docker\" is selected.\n- Select a server location close to you. I'm near Frankfurt, but if you are in the US or Asia you might want to choose a different one so it's faster to connect to.\n- Select the \"Free\" server option.\n\nAt the bottom of the service there is an \"Advanced\" section which you can use to further configure your service. We'll talk more about that in a bit.\n\nFor now, hit \"Create Web Service\" and wait for it to deploy your code from GitHub!\n\nIf you navigate to your Dashboard and then click through to your newly created service, you'll be able to see the service details. If it isn't already deploying, click on the \"Manual Deploy\" button on the top right to initiate a deploy of the latest commit:\n\n![](https://res.cloudinary.com/teclado/image/upload/v1689180775/courses/rest-apis-flask-python/deploy-latest-commit_k9as13.png)\n\nThen you should start seeing logs appear detailing the deployment process!\n\n![](https://res.cloudinary.com/teclado/image/upload/v1689180776/courses/rest-apis-flask-python/render-deploy-screen_lfx1uh.png)\n\nWhile on the free plan, deployments are a bit slow. It has to build your image and run it! Give it a few minutes, until the deployment succeeds. You should see this:\n\n![](https://res.cloudinary.com/teclado/image/upload/v1689180776/courses/rest-apis-flask-python/render-deploy-finished_lyiftz.png)\n\nNow, you can access your service URL and try it out using Insomnia or Postman!\n\n:::warning\nIf `data.db` is in the `.gitignore` file you need to call  `flask db migrate` to populate the sqlite database. Since the free plan does not provide the CLI, you can do this in the `Dockerfile` by adding `RUN flask db upgrade` after `COPY . .`.\n:::\n\n![](https://res.cloudinary.com/teclado/image/upload/v1689180778/courses/rest-apis-flask-python/insomnia-test-prod_dlfe1d.png)\n\n:::warning\nFree services in Render.com shut down after inactivity for a few minutes. If you don't use your service for a few minutes, it will shut down and it will need to restart, which can take a minute! This is one of the limitations of their free plan.\n:::\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/03_docker_with_gunicorn/README.md",
    "content": "---\nctslug: run-our-flask-app-with-gunicorn\n---\n\n# Run our Flask app with gunicorn in Docker\n\nThroughout the course, we've been working with a Docker image like this one:\n\n```dockerfile\nFROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]\n```\n\nThis is all well and good for local development, but when we deploy our application we want to run it with the best performance possible.\n\nThis is why we don't want to run the Flask development server and the Flask debugger. Instead, we'll use gunicorn to run our app.\n\n## Run our Flask app with gunicorn\n\nFirst let's add `gunicorn` to our `requirements.txt` file:\n\n```text title=\"requirements.txt\"\nflask\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\n# highlight-start\ngunicorn\n# highlight-end\n```\n\nThen, let's change our `Dockerfile` to use `gunicorn`:\n\n```dockerfile\nFROM python:3.10\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\n# highlight-start\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\n# highlight-end\nCOPY . .\n# highlight-start\nCMD [\"gunicorn\", \"--bind\", \"0.0.0.0:80\", \"app:create_app()\"]\n# highlight-end\n```\n\nThe `CMD` line change is the important one, as it runs `gunicorn` on port `80`, and we pass in the app factory function.\n\n:::tip\nNote I've also changed the `pip install` line. Adding `--no-cache-dir` and `--upgrade` just makes sure we can't accidentally install from a cache directory (which shouldn't exist anyway!), and that we'll upgrade to the latest possible versions allowed by our `requirements.txt` file.\n:::\n\n## Run the Docker container locally with the Flask development server and debugger\n\nIf you use this `Dockerfile`, it doesn't mean you can't run it locally using the Flask development server. You don't have to lose the automatic restarting capabilities, or the Flask debugger.\n\nTo run the Docker container locally, you'll have to do this from now on:\n\n```zsh\ndocker run -dp 5000:5000 -w /app -v \"$(pwd):/app\" teclado-site-flask sh -c \"flask run --host 0.0.0.0\"\n```\n\nThis is similar to how we've ran the Docker container with our local code as a volume (that's what `-w /app -v \"$(pwd):/app\"` does), but at the end of the command we're telling the container to run `flask run --host 0.0.0.0` instead of the `CMD` line of the `Dockerfile`. That's what `sh -c \"flask run --host 0.0.0.0\"` does!\n\nNow you're ready to commit and push this to your repository and re-deploy to Render.com!\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/04_deploy_postgresql_database/README.md",
    "content": "---\nctslug: how-to-get-a-deployed-postgresql-database\n---\n\n# How to get a deployed PostgreSQL database for our app\n\nThere are many PostgreSQL-as-a-Service providers (that means, services that handle creating and maintaining your database for you).\n\nRender.com offers PostgreSQL, and the pricing is actually quite fair. However, the free tier is limited and you can only use it for a certain amount of time.\n\nThat's why I recommend using ElephantSQL for your free PostgreSQL needs. When you go over the free ElephantSQL limits, then you can use the Render.com database instead.\n\nTo get a free ElephantSQL PostgreSQL database, just go to their site, sign up, and then create a database in a region close to your Render.com server. Make sure to select the free tier.\n\n![ElephantSQL screenshot showing plan configuration of Tiny Turtle (free) and name](https://res.cloudinary.com/teclado/image/upload/v1689180780/courses/rest-apis-flask-python/select-plan-and-name-elephantsql_sx3v2e.png)\n\nOnce you've got this, you should be able to see the Database URL:\n\n![ElephantSQL screenshot showing that a copy icon beside the Database URL can be clicked to copy it](https://res.cloudinary.com/teclado/image/upload/v1689180780/courses/rest-apis-flask-python/copy-elephantsql-url_ydr90k.png)\n\nCopy this, as you'll need it in the next lecture!\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md",
    "content": "---\nctslug: how-to-use-environment-variables-in-render-com\n---\n\n# How to use Environment Variables in Render.com\n\nA common way to configure applications before they start up is by using environment variables.\n\nWe can define environment variables in our computers, and also in our servers, and of course they can be different in each.\n\nThat's what's interesting about them: we can define an environment variable locally for our database, which may be `sqlite:///data.db`. Then in our server we can define the same variable, but with a value of the ElephantSQL Database URL.\n\nSince we are using SQLAlchemy in our application, it doesn't care whether it's connecting to SQLite or PostgreSQL. So all we have to do to use a different database is change the connection string.\n\nLet's begin by using environment variables locally.\n\n## Using PostgreSQL locally\n\nSince we are going to be using PostgreSQL when we deploy, it's a good idea to use PostgreSQL also locally. That's because SQLite and PostgreSQL behave a bit differently, so if we use SQLite locally and PostgreSQL in production, we may come across issues.\n\nTo work with PostgreSQL locally, you can run a PostgreSQL container using Docker, you can install PostgreSQL locally, or you can create another ElephantSQL database for local development.\n\nI would do the last option. That way, you'll have 2 ElephantSQL databases; one for production and one for development.\n\n## How to use environment variables locally with our Flask app\n\nFirst let's install `psycopg2` and add it to our `requirements.txt` file:\n\n```text title=\"requirements.txt\"\nflask\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\n# highlight-start\npsycopg2\n# highlight-end\n```\n\nThen, let's create a new file called `.env`. In this file, we can store any environment variables we want. We can then \"load\" these variables when we start the app.\n\n```text title=\".env\"\nDATABASE_URL=postgresql://YOUR_DEVELOPMENT_URL\n```\n\n:::warning\nThe ElephantSQL URL starts with `postgres://...`. Make sure to change it so it starts with `postgresql://...`!\n:::\n\nWith the file created, we can load it when we start our Flask app:\n\n```python title=\"app.py\"\n# highlight-start\nimport os\n# highlight-end\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n# highlight-start\nfrom dotenv import load_dotenv\n# highlight-end\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    # highlight-start\n    load_dotenv()\n    # highlight-end\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    # highlight-start\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    # highlight-end\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n```\n\nHighlighted are four lines which we must change.\n\n1. First we `import os`. We'll need this to access environment variables.\n2. Second, we import the `load_dotenv` function, which we'll need to run in order to turn the contents of the `.env` file into environment variables.\n3. We actually run the `load_dotenv` function.\n4. We'll use `db_url` if provided, otherwise we'll retrieve the environment variable's value. If there is no environment value, the default will be `\"sqlite:///data.db\"`.\n\nNotice that our Flask app has two ways to be configured: with the `db_url` argument, or via environment variables. You would normally use `db_url` when writing automated tests for your application. While we don't do that in this course, it's a good habit to get into!\n\n:::warning\nDo not include your `.env` file in your GitHub repository! Add it to `.gitignore` so you don't include it accidentally.\n:::\n\nSince we can't include `.env` in our GitHub repository, we should do something to make sure that new developers know that they should create a `.env` file when they clone the repository.\n\nWe normally do this by creating a file called `.env.example`. This file should only contain the environment variable definitions, but not the values:\n\n```text title=\".env.example\"\nDATABASE_URL=\n```\n\nYou should add `.env.example` to your repository.\n\n## Changes needed to our app code for PostgreSQL\n\nWe've been working with SQLite all this time, and PostgreSQL behaves a bit differently. There are a couple of changes we need to make to our app at this point:\n\n1. Make sure all foreign keys are the same data type as the primary keys they reference.\n2. Change the length constraint on user passwords from `80` to `256`.\n\nThis is because SQLite doesn't enforce either of these constraints, so although they were a problem before, we didn't know because SQLite didn't tell us about it. PostgreSQL will complain!\n\n### Changes to foreign keys\n\nThe only foreign key that was mistakenly given the wrong data type was in the `TagModel`. This is the necessary change:\n\n```python title=\"models/tag.py\"\nfrom db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    # highlight-start\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n    # highlight-end\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n```\n\nWe also need to change the database migration file that creates the store ID:\n\n```python title=\"migrations/versions/07006e31e788_.py\"\n...\n\nop.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    # highlight-start\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    # highlight-end\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n\n...\n```\n\nNow, let's run the migrations so that our development ElephantSQL database is created. Remember to make sure that your development ElephantSQL database is empty before starting the migrations.\n\n```bash\nflask db upgrade\n```\n\n### Changes to password length\n\nIn the `UserModel`, we'll make this change:\n\n```python title=\"models/user.py\"\nfrom db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    # highlight-start\n    password = db.Column(db.String(256), nullable=False)\n    # highlight-end\n```\n\n### Running our migration with string length changes\n\nNow we want to create a new migration so that our changes to the `UserModel` will be applied:\n\n```bash\nflask db migrate\n```\n\nThis may add a couple other data type changes, such as changing `REAL` to `Float`. This is due to how types are assigned differently between SQLite and PostgreSQL. Make sure that the password length change is in the migration:\n\n```python title=\"migrations/versions/36e961f62882_.py\"\nop.alter_column('users', 'password',\n    existing_type=sa.VARCHAR(length=80),\n    type_=sa.String(length=256),\n    existing_nullable=False)\n```\n\n## Running database migrations in production\n\nSo we've created our migration files and we've migrated our development database. What about our production database?\n\nWe _could_ simply change our `.env` file, connect to production, and migrate that database. But then we'd need to remember to do that every time before we deploy, and it simply isn't feasible.\n\nInstead, we want a solution where the database migrations run before the app starts. That way, it will be impossible for us to forget to run the migrations when we deploy.\n\nTo do so, we'll tell the Docker container to run the database migrations before starting the `gunicorn` process. It's more straightforward than it sounds!\n\nFirst let's write a very short bash script that runs the migrations, and then starts the gunicorn process:\n\n```bash title=\"docker-entrypoint.sh\"\n#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\"\n```\n\nThen, let's modify our `Dockerfile` to use that script:\n\n```dockerfile\nFROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\n# highlight-start\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]\n# highlight-end\n```\n\n:::tip\nIf you want to run the Docker container locally with the Flask development server, our [previous instructions](/docs/deploy_to_render/docker_with_gunicorn/#run-the-docker-container-locally-with-the-flask-development-server-and-debugger) are still good. You won't be applying the migrations, but most of the time that won't be a problem.\n:::\n\nCommit the changes, and push them to GitHub. We'll need these changes so we can use environment variables in Render.com.\n\n## How to add environment variables to Render.com\n\nNow that our Flask app is using environment variables, all we have to do is add the `DATABASE_URL` environment variable to our Render.com service, and then deploy the latest changes from our GitHub repository.\n\nTo add environment variables in Render.com, go to the service settings and then on the left you'll see \"Environment\":\n\n![Render.com screenshot showing the button to add a environment variables](https://res.cloudinary.com/teclado/image/upload/v1689180783/courses/rest-apis-flask-python/render-add-env-var_lodpee.png)\n\nClick on \"Add Environment Variable\", and there put `DATABASE_URL` as the key, and your ElephantSQL Database URL as the value:\n\n![Render.com screenshot showing DATABASE_URL added with a pixelated value](https://res.cloudinary.com/teclado/image/upload/v1689180784/courses/rest-apis-flask-python/render-database-url-env-var_wrxgjl.png)\n\n:::warning\nAgain, make sure to use `postgresql://...` here.\n:::\n\nNow, do another manual deploy of the latest commit.\n\nWhen this is done, your app should be saving to the ElephantSQL database, and it will apply the migrations before starting up!\n\n[^alembic_docs]: [Compare Types (Alembic official documentation)](https://alembic.sqlalchemy.org/en/latest/autogenerate.html#compare-types)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/README.md",
    "content": "# How to run the app and database with Docker Compose\n\nUp until now we've been running `docker compose up` to start the REST API container.\n\nNow let's modify our `docker-compose.yml` file to include spinning up a new PostgreSQL database.\n\n```yaml\nservices:\n  web:\n    build: .\n    ports:\n      - \"5000:80\"\n    depends_on:\n      db:\n        condition: service_healthy\n    env_file:\n      - ./.env\n    volumes:\n      - .:/app\n  db:\n    image: postgres\n    environment:\n      - POSTGRES_PASSWORD=password\n      - POSTGRES_DB=myapp\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    healthcheck:\n      test: pg_isready -d $${POSTGRES_DB} -U postgres\n      interval: 2s\n      retries: 10\nvolumes:\n  postgres_data:\n```\n\nThe `postgres` image accepts various environment variables, among them:\n\n- `POSTGRES_PASSWORD`, defaulting to `postgres`\n- `POSTGRES_DB`, defaulting to `postgres`\n- `POSTGRES_USER`, defaulting to `postgres`\n- `POSTGRES_HOST`, defaulting to `localhost`\n- `POSTGRES_PORT`, defaulting to `5432`\n\nWe should at least set a secure password. Above we're changing the password and database to `password` and `myapp` respectively.\n\n:::caution\nRemember to also change your `DATABASE_URL` in your `.env` file that the REST API container is using. It should look like this:\n\n```\nDATABASE_URL=postgresql://postgres:password@db/myapp\n```\n\nWhen Docker Compose runs, it creates a virtual network[^1] which allows you to connect to `db`, which connects to the running `db` service container.\n:::\n\nIn the `docker-compose.yml` file above you can also see that the `web` service depends on the `db` service, with the condition that it is healthy. A service is deemed \"healthy\" when its healthcheck passes.\n\nWe've added a healthcheck to the `db` service which runs the `pg_isready`[^2] program using the supplied database and PostgreSQL user. This just tells us whether the PostgreSQL server is ready to respond to requests.\n\nAdding this means the `web` service won't start until the `db` service is ready to respond to requests.\n\n## Named volumes in Docker Compose\n\nYou'll notice that our `docker-compose.yml` file has these lines:\n\n```\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\nvolumes:\n  postgres_data:\n```\n\nThe 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.\n\nIn 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.\n\n`/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.\n\nWhen you restart the container (or even rebuilt it), you can use the same named volume to keep access to old data.\n\nIf you want to delete the entire database content, you can do so by deleting the volume through Docker Desktop, or with this command:\n\n```\ndocker compose down -v\n```\n\n## Starting the whole system\n\nNow you're ready to start the Docker Compose system! If you need to rebuild the REST API container first, run:\n\n```\ndocker compose up --build --force-recreate --no-deps web\n```\n\nYou'll get an error due to no database being available. That's OK, as long as the container is rebuilt!\n\nThen press `CTRL+C` to stop it, and start the whole system with:\n\n```\ndocker compose up\n```\n\nNow you can make a request to your API on port 5000, and it should work, storing the data in the database!\n\n## Running the system in background mode\n\nWhen we run the system with `docker compose up`, it takes up the terminal until we stop it with `CTRL+C`.\n\nIf you want to run it in \"Daemon\" mode, or in the background, so you can use the terminal for other things, you can use:\n\n```\ndocker compose up -d\n```\n\nThen to stop the system, use:\n\n```\ndocker compose down\n```\n\nNote you must be in the folder that contains your `docker-compose.yml` file in order to bring the system up or down.\n\n:::warning\nRunning `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.\n:::\n\n[^1]: [Networking in Compose (official docs)](https://docs.docker.com/compose/networking/)\n[^2]: [pg_isready (PostgreSQL documentation)](https://www.postgresql.org/docs/current/app-pg-isready.html)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-compose.yml",
    "content": "services:\n  web:\n    build: .\n    ports:\n      - \"5000:80\"\n    depends_on:\n      - db\n    env_file:\n      - ./.env\n    volumes:\n      - .:/app\n  db:\n    image: postgres\n    environment:\n      - POSTGRES_PASSWORD=password\n      - POSTGRES_DB=myapp\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\nvolumes:\n  postgres_data:"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, jwt_required, get_jwt\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n        \n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"])\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n        \n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, jwt_required, get_jwt\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n        \n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"])\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n        \n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200"
  },
  {
    "path": "docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)"
  },
  {
    "path": "docs/docs/11_deploy_to_render/Insomnia_section11.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-18T22:14:08.036Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_92cec23efa0d47a582b4cf476fbe1c7d\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660695,\"created\":1668007660695,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660692,\"created\":1668007660692,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"parentId\":null,\"modified\":1668007660646,\"created\":1668007660646,\"name\":\"Section 11\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_70c9c31ae27e47b684428bb88932f96d\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660702,\"created\":1668007660702,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_a9790bade2c14dd5a87b85088017aa92\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668809637607,\"created\":1668007660694,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0aa8a2f045e94cea96ba0f7061a26c41\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668678383934,\"created\":1668007660698,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1a5f0c9aa22841e0a60e47c6a5f65edc\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668678384453,\"created\":1668007660701,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_969a6a61d9b74bd3ad27623663999dca\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660697,\"created\":1668007660697,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8b7c05ec34874814bbb1ed17f23d7ea3\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660682,\"created\":1668007660682,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660679,\"created\":1668007660679,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_c063f1996fde409f81d62a0e3d20b631\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660681,\"created\":1668007660681,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_007ed54990944618a9e4bbf4fed9bdc8\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660686,\"created\":1668007660686,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5739a9e1be2e4fcd8ecdac5142b5e76d\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668678381399,\"created\":1668007660684,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fbc4905447b04feca058e6f8c0428acf\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660688,\"created\":1668007660688,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_839409cd69e945b694d46f3101eec0d5\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660690,\"created\":1668007660690,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fe2a4b3d67664cd194efe09393b9fd02\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660660,\"created\":1668007660660,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660658,\"created\":1668007660658,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"req_b34295f6d8b24000b77f812c9cc2c2cc\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660663,\"created\":1668007660663,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_90015fc3fcae4301b0d8a5c0355a3d83\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668678375918,\"created\":1668007660661,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0e20a55b16034f06a3246c4e76f1c387\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660665,\"created\":1668007660665,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_671694da515a4c4e877008b02cd17f57\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660673,\"created\":1668007660673,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660668,\"created\":1668007660668,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_a55050436885439f8393e70546d75641\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660676,\"created\":1668007660676,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b8cc399abc8749eea66c6967522f8a36\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668678378059,\"created\":1668007660669,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8e9e92854eab4e74af765725b54c7d31\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660671,\"created\":1668007660671,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5303d646b4c44843bc9c8322675f9c54\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668678378608,\"created\":1668007660675,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_e09818240d524d079150f6fc00963c54\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660648,\"created\":1668007660648,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_9aa71c77793345cabd413a66ad557bd7\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660652,\"created\":1668007660652,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_2f42797dc9c6441fb3d3e7b23780dc27\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660725,\"created\":1668007660654,\"fileName\":\"Section 11\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/11_deploy_to_render/_category_.json",
    "content": "{\n    \"label\": \"Deploy REST APIs to Render\",\n    \"position\": 11\n}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/README.md",
    "content": "---\nctslug: how-to-send-emails-with-python-mailgun\n---\n\n# How to send emails with Python and Mailgun\n\n:::tip Insomnia files\nRemember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!\n:::\n\nTo send e-mails using Python, we are going to use Mailgun, a third party service which actually delivers the messages.\n\nYou could use [your own personal account and the built-in `email` and `smtp` libraries](https://blog.teclado.com/learn-python-send-emails/), but most personal e-mail providers will limit how many e-mails you can send per day. Plus, you won't get analytics and a host of other features that you can get with an email service like Mailgun.\n\nThere are two ways to use the Mailgun service: [via SMTP or via their API](https://www.mailgun.com/blog/email/difference-between-smtp-and-api/). I'll show you how to use the API since it's a bit easier and has the same functionality.\n\nSending an e-mail with Mailgun is just a matter of sending a request to their API. To do this, we'll use the `requests` library:\n\n```bash\npip install requests\n```\n\nRemember to add it to your `requirements.txt` as well:\n\n```text title=\"requirements.txt\"\nrequests\n```\n\n## Setting up for Mailgun\n\nBefore we can send any emails, we need to set up our Mailgun account. First, register over at [https://mailgun.com](https://mailgun.com).\n\nOnce you have registered, select your sandbox domain. It's in [your dashboard](https://app.mailgun.com/app/dashboard), at the bottom. It looks like this: `sandbox847487f8g78.mailgun.org`. \n\nThen at the top right, enter your personal email address under \"Authorized recipients\".\n\nYou will get an email to confirm. Click the button that you see in that email to add your personal email to the list of authorized recipients.\n\nNext up, grab your API key. You can find it by clicking on this button (my domain and API key are blurred in this screenshot):\n\n![Click the 'Select' button to reveal your Mailgun API key](https://res.cloudinary.com/teclado/image/upload/v1689180789/courses/rest-apis-flask-python/mailgun-api-key_mc8bjz.png)\n\n## Sending emails with Mailgun\n\nTo make the API request which sends an email, we'll use a function that looks very much like this one (taken from their documentation):\n\n```py\ndef send_simple_message():\n    return requests.post(\n        \"https://api.mailgun.net/v3/YOUR_DOMAIN_NAME/messages\",\n        auth=(\"api\", \"YOUR_API_KEY\"),\n        data={\"from\": \"Excited User <mailgun@YOUR_DOMAIN_NAME>\",\n            \"to\": [\"bar@example.com\", \"YOU@YOUR_DOMAIN_NAME\"],\n            \"subject\": \"Hello\",\n            \"text\": \"Testing some Mailgun awesomness!\"})\n```\n\nSo let's go into our User resource and add a couple of imports and this function. Make sure to replace \"Your Name\" with your actual name or that of your application:\n\n```py title=\"resources/user.py\"\nimport os\nimport requests\n\n...\n\ndef send_simple_message(to, subject, body):\n    domain = os.getenv(\"MAILGUN_DOMAIN\")\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{domain}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{domain}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n```\n\nThen let's go to the `.env` file and add your Mailgun API key and domain:\n\n```text title=\".env\"\nMAILGUN_API_KEY=\"<insert your api key here>\"\nMAILGUN_DOMAIN=\"<insert your domain here>\"\n```\n\n:::info\nThe API Key should look something like this: `\"1f1ahfjhf4878797887187j-5ac54n\"`.\n\nThe Domain should look something like this: `\"sandbox723b05d9.mailgun.org\"`\n:::\n\nWith this, we're ready to actually send emails!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/resources/user.py",
    "content": "import os\nimport requests\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\ndef send_simple_message(to, subject, body):\n    domain = os.getenv(\"MAILGUN_DOMAIN\")\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{domain}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{domain}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, jwt_required, get_jwt\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n        \n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"])\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n        \n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200"
  },
  {
    "path": "docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/README.md",
    "content": "---\nctslug: sending-emails-when-users-register\n---\n\n# Sending emails when users register\n\nIf we want to be able to send emails to users when they register, we'll need to:\n\n- Add an `email` column to the user model.\n- Collect user email addresses when users register.\n\nLet's begin with the model.\n\n## Add an `email` column to the user model\n\n```diff title=\"models/user.py\"\n+    email = db.Column(db.String, unique=True, nullable=False)\n```\n\nThen run the migration as we've already learned, to generate the migration script and upgrade the database to include the new column:\n\n```bash\nflask db migrate\n```\n\nNow let's check the migration script. It should include adding the `email` column, and making it unique.\n\nMake sure that the `UniqueConstraint` is given a name. Alembic won't do this for you. Instead, it gives it the name `None` by default:\n\n```py\nop.create_unique_constraint(None, 'users', ['email'])\n```\n\nChange that to this:\n\n```py\nop.create_unique_constraint(\"email\", 'users', ['email'])\n```\n\nAnd also when dropping the constraint:\n\n```py\nop.drop_constraint(\"email\", 'users', type_='unique')\n```\n\n```bash\nflask db upgrade  # make sure this is using the local dev database\n```\n\n## Collect user email addresses when they register\n\nTo do this, first let's add an `email` field to the incoming data. Remember that we use the `UserSchema` for this in our API, but at the moment we are using `UserSchema` for two things: registration and login.\n\nIf we modify `UserSchema` to add an email field, users will need to give us their username, email, and password when they log in.\n\nSo it's better to keep two schemas: one for registration, which asks for an email, and one for login, which only asks for the username.\n\n```py\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True, load_only=True)\n\n\n# highlight-start\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n# highlight-end\n```\n\n:::info\nYou could also get rid of usernames and only use emails. You can use email/password for login in that case!\n:::\n\nNow that we've got that, we can actually use the email field to create our `UserModel` objects:\n\n```py title=\"resources/user.py\"\nfrom schemas import UserSchema, UserRegisterSchema\n\n...\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    # highlight-start\n    @blp.arguments(UserRegisterSchema)\n    # highlight-end\n    def post(self, user_data):\n...\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            # highlight-start\n            email=user_data[\"email\"],\n            # highlight-end\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n```\n\nNow we can use the `send_simple_message` function [we defined earlier](../01_send_emails_python_mailgun/README.md#sending-emails-with-mailgun) to actually send an email!\n\n```py title=\"resources/user.py\"\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        # highlight-start\n        send_simple_message(\n            to=user.email,\n            subject=\"Successfully signed up\",\n            body=f\"Hi {user.username}! You have successfully signed up to the Stores REST API.\"\n        )\n        # highlight-end\n\n        return {\"message\": \"User created successfully.\"}, 201\n```\n\n## Error handling duplicate emails\n\nIn our `UserRegister` resource we are checking for duplicate usernames, but we should also check for duplicate emails. Otherwise, if a user tries to sign up with an email that already exists in the database, they'll get an ugly error.\n\n```py title=\"resources/user.py\"\nfrom sqlalchemy import or_\n\n...\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"]\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n        \n        # ... Method continues here ...\n```\n\nSo voilà, we're now sending an email when a user signs up!\n\nBut sending an email can take a non-trivial amount of time... Wouldn't it be nice if we could offload the task of sending emails to another process, so that it happens in the background without our API user having to wait?"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/.python-version",
    "content": "3.10.6\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"bb5da1e68550\"\ndown_revision = \"8ca023a4a4b0\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column(\n        \"items\",\n        \"price\",\n        existing_type=sa.REAL(),\n        type_=sa.Float(precision=2),\n        existing_nullable=False,\n    )\n    op.alter_column(\n        \"users\",\n        \"password\",\n        existing_type=sa.VARCHAR(length=80),\n        type_=sa.String(length=256),\n        existing_nullable=False,\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column(\n        \"users\",\n        \"password\",\n        existing_type=sa.String(length=256),\n        type_=sa.VARCHAR(length=80),\n        existing_nullable=False,\n    )\n    op.alter_column(\n        \"items\",\n        \"price\",\n        existing_type=sa.Float(precision=2),\n        type_=sa.REAL(),\n        existing_nullable=False,\n    )\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/resources/user.py",
    "content": "import os\nimport requests\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\ndef send_simple_message(to, subject, body):\n    domain = os.getenv(\"MAILGUN_DOMAIN\")\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{domain}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{domain}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        send_simple_message(\n            to=user.email,\n            subject=\"Successfully signed up\",\n            body=f\"Hi {user.username}! You have successfully signed up to the Stores REST API.\",\n        )\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/resources/user.py",
    "content": "import os\nimport requests\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\ndef send_simple_message(to, subject, body):\n    domain = os.getenv(\"MAILGUN_DOMAIN\")\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{domain}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{domain}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(UserModel.username == user_data[\"username\"]).first():\n            abort(409, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/02_send_email_user_registration/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/03_what_is_task_queue/README.md",
    "content": "--- \nctslug: what-is-a-task-queue\n---\n\n# What is a task queue?\n\nA queue is a data structure to which you can add and remove data, but a key aspect of it is that when you want to remove a piece of data from it, the piece of data removed is the first piece of data that was added.\n\n![New elements are added at the end, called pushing, and removed from the start, called popping, of a queue](https://res.cloudinary.com/teclado/image/upload/v1689180793/courses/rest-apis-flask-python/queues.drawio_yqgtvg.png)\n\nThis is identical to how people queuing works. The first person to arrive at the queue (i.e. the first in line), is the first person removed from the queue when they reach the ticket counter.\n\nWe need a queueing system for our email sending so that when we offload tasks, we put them in a queue. Then we will have a separate program (the **background worker**), taking items from the queue one at a time and processing them.\n\nEach item in the queue will be an email to be sent (or rather, information so that the background worker can send the email).\n\n## Setting up the Redis database for our queue\n\nWe can use the Redis database to store our queue of tasks. There are alternative options, such as RabbitMQ, but we won't cover them in this course.\n\nYou can install Redis in a few different ways:\n\n- Install it locally by following their guides.\n- Install it using Docker (I recommend this for a local install).\n- Use a Redis database in the cloud so you don't have to install anything (this is what we do in the video).\n\nRender.com can provide us with a free Redis database, so I recommend using that to get started.\n\nNavigate to your Render.com dashboard, and create a new free Redis database. The free Redis provided doesn't have persistence enabled, but that's okay. It means we will lose data if the service is turned off, but since we're using it as a task queue that's not as big a deal as it otherwise could be.\n\nLater on if we want, we can upgrade to one of the paid plans.\n\nTo be able to add tasks to the queue from your dev environment, make sure to [allow external connections](https://render.com/docs/redis#connecting-to-your-redis-from-outside-render) in your Redis database configuration.\n\n![Screenshot showing 0.0.0.0/0 as an allowed IP address when connecting to our Render Redis database](https://res.cloudinary.com/teclado/image/upload/v1689180794/courses/rest-apis-flask-python/render-redis-allowing-outside_tkxsls.png)\n\nYou should get a Redis URL that looks like this: `rediss://red-ct8aen0hkl10:MnLs0mmrX7MBXWRkdrh49@frankfurt-redis.render.com:6379`. Save it, for we'll need it in the next lecture!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/README.md",
    "content": "---\nctslug: populating-consuming-the-task-queue-with-rq\n---\n\n# Populating and consuming the task queue with rq\n\nWe'll be using the [`rq` library](https://python-rq.org/) for our task queue implementation. Another popular option is `celery`, which is substantially more complex. For most workloads, `rq` is sufficient and it's much easier to work with.\n\nFirst install the library:\n\n```bash\npip install rq\n```\n\nAnd remember to add it to your `requirements.txt`\n\n```text title=\"requirements.txt\"\nrq\n```\n\nThen it's helpful if we move the task code out to a separate file. Let's take our `send_simple_message` function and move it to `tasks.py`:\n\n```py title=\"tasks.py\"\nimport os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body}\n    )\n```\n\nHere I moved the domain line outside the function so it only runs once, and I've made sure to run `load_dotenv()` before it is requested.\n\nThe background worker will import `tasks.py` once at the start of its lifetime, so doing this will (very slightly) improve performance.\n\nWe could leave it like this, but I think we can do better. Let's write another function underneath that one that specifically describes the task that we want to perform in the background: send a registration email to a specific user:\n\n```py title=\"tasks.py\"\nimport os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body}\n    )\n\n\n# highlight-start\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n    )\n# highlight-end\n```\n\n:::tip\nRemember to change \"Your Name\" in `from` to whatever name you want your emails to come from!\n:::\n\nNext up, add the Redis connection string that we got in the [previous section](../what_is_task_queue) to the `.env` file:\n\n```text title=\".env\"\nREDIS_URL=\"<insert your Redis url here>\"\n```\n\nAnd then let's go to our User resource and add a couple of imports:\n\n```py title=\"resources/user.py\"\nimport redis\nfrom rq import Queue\nfrom tasks import send_user_registration_email\n```\n\nThen let's connect to Redis and create our `rq` queue. Under the blueprint definition, I'll add these lines:\n\n```py title=\"resources/user.py\"\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n```\n\nNow we can use the `queue` to \"enqueue\" jobs, i.e. add to the queue. That will put some data into the Redis database, which then the background worker can consume.\n\n### How to enqueue a job using `rq`\n\nThis is the easy part!\n\nWe are going to remove the code that sends the email from `resources/user.py`, and instead enqueue it using the `queue` variable. This takes the name of the function we want the background worker to call, and then all the arguments we'd like to pass to that function when it runs.\n\n```diff title=\"resources/user.py\"\n-send_simple_message(\n-    to=user.email,\n-    subject=\"Successfully signed up\",\n-    body=f\"Hi {user.username}! You have successfully signed up to the Stores REST API.\"\n-)\n+queue.enqueue(send_user_registration_email, user.email, user.username)\n```\n\n:::info\nRemember the `send_user_registration_email` function doesn't run when we call `.enqueue`. It runs when the background worker starts working on this task, which could take some time!\n:::"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/resources/user.py",
    "content": "import os\nimport requests\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\ndef send_simple_message(to, subject, body):\n    domain = os.getenv(\"MAILGUN_DOMAIN\")\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{domain}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{domain}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        send_simple_message(\n            to=user.email,\n            subject=\"Successfully signed up\",\n            body=f\"Hi {user.username}! You have successfully signed up to the Stores REST API.\",\n        )\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/README.md",
    "content": "--- \nctslug: process-background-tasks-with-the-rq-worker\n---\n\n# Process background tasks with the rq worker\n\nWe've got our queue and we've added tasks to it, but they won't run until we start consuming them and popping them off the queue.\n\nTo do this, we'll run a background worker whose job it is to pop items off the queue one at a time, and run the associated Python function with the associated arguments.\n\n:::tip MacOS or Linux?\nIf you are using MacOS or Linux, you can run the background worker for testing using this command (make sure your virtual environment is active):\n\n```bash\nrq worker -u <insert your Redis url here> emails\n```\n\nThe `rq` executable is available after installing the `rq` library with `pip`. The `-u` flag gives it the Redis URL to connect to. The `emails` at the end is the name of the queue that it should consume from. Make sure it matches the name of the queue you defined in `resources/user.py`.\n:::\n\n:::warning Running on MacOS\nYou may get an error when running `rq worker` directly using MacOS (without Docker):\n\n```text\nobjc[21400]: +[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called.\n```\n\nIf so, try running this command before starting your `rq worker`:\n\n```bash\nexport OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES\n```\n\n:::\n\nThe most reliable way to run the worker though, is using Docker.\n\nWe are already used to running our API using Docker, so now we can use the same Docker image to run our worker.\n\nFirst, build the image:\n\n```bash\ndocker build -t rest-apis-flask-smorest-rq .\n```\n\nThen run a container, but instead of running the default entrypoint (defined by the `CMD` line in the `Dockerfile`), we'll tell it to run the `rq` program:\n\n```bash\ndocker run -w /app rest-apis-flask-smorest-rq sh -c \"rq worker -u <insert your Redis url here> emails\"\n```\n\nThis ensures one of the [considerations](https://python-rq.org/docs/#considerations-for-jobs) that the `rq` documentation suggests: that the worker and the work generator (our API) share _exactly_ the same source code.\n\nRun another Docker container for your API, and try to register!\n\nimport Tabs from '@theme/Tabs';\nimport TabItem from '@theme/TabItem';\n\n<div className=\"codeTabContainer\">\n<Tabs>\n<TabItem value=\"app\" label=\"Run the app\" default>\n\n```bash\ndocker run -p 5000:5000 rest-apis-flask-smorest-rq sh -c \"flask run --host 0.0.0.0\"\n```\n\n</TabItem>\n<TabItem value=\"worker\" label=\"Run the background worker\">\n\n```bash\ndocker run -w /app rest-apis-flask-smorest-rq sh -c \"rq worker -u <insert your Redis url here> emails\"\n```\n\n:::info\nMake sure to enter your own Redis connection string in that command!\n:::\n\n</TabItem>\n</Tabs>\n</div>"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/end/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/05_rq_background_worker/start/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/README.md",
    "content": "---\nctslug: sending-html-emails-with-mailgun\n---\n\n# Sending HTML emails with Mailgun\n\nUntil now, we've been sending exclusively text emails. These have a clear advantage: text is simple! They'll look the same in every email client and device, and for many things, text is good enough.\n\nHowever, I'll be the first to say that it doesn't look amazing. You're at the mercy of the default font family and size of the recipient's email client, and you can't personalize the email with your business branding.\n\nThis is where HTML emails come into play.\n\nHTML emails require that we write HTML instead of text, and also CSS for the styling. We should still keep the text version of the email, just in case the recipient's email client doesn't render HTML for whatever reason.\n\n## Writing HTML emails\n\nCrafting HTML emails is difficult! Every email client renders things slightly differently and supports different versions of the HTML and CSS specs.\n\nFor example, it's discouraged to use CSS Flex when writing emails, because many email clients don't support it.\n\nThat's why you'll see most HTML emails use HTML tables for their layout 🤮\n\nFortunately for us, Mailgun provides a few [HTML templates](https://www.mailgun.com/blog/email/transactional-html-email-templates/) that we can simply copy, paste, and modify. They test these HTML templates to make sure they render correctly in most email clients, and they come with CSS already written.\n\n## Getting the Mailgun HTML email templates\n\nThis link has a writeup of how HTML templates work: [https://www.mailgun.com/blog/email/transactional-html-email-templates/](https://www.mailgun.com/blog/email/transactional-html-email-templates/).\n\nYou can find their templates here: [https://github.com/mailgun/transactional-email-templates/tree/master/templates/inlined](https://github.com/mailgun/transactional-email-templates/tree/master/templates/inlined).\n\nThere are three different transactional email templates, and we'll be using the [`action.html`](https://raw.githubusercontent.com/mailgun/transactional-email-templates/master/templates/inlined/action.html) template in this lecture for our \"user registration\" email.\n\n## Adding the template to our application\n\nCreate a `templates/email/action.html` file in your project, and place the entire raw code of the `action.html` file from the Mailgun repository.\n\n:::tip\nMake sure to grab the [**raw** code](https://raw.githubusercontent.com/mailgun/transactional-email-templates/master/templates/inlined/action.html) to make sure there are no GitHub artefacts in the code.\n:::\n\nThe copied [`action.html`](https://github.com/mailgun/transactional-email-templates/blob/master/templates/inlined/action.html) code from the [Mailgun repository](https://github.com/mailgun/transactional-email-templates) (below) is licensed with the MIT license. Please see the [repository license](https://github.com/mailgun/transactional-email-templates/blob/master/LICENSE) for more information.\n\n```html title=\"templates/email/action.html\"\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Actionable emails e.g. reset password</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tPlease confirm your email address by clicking the link below.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWe may need to send you critical information about our service and it is important that we have an accurate email address.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://www.mailgun.com\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Confirm email address</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; The Mailgunners\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/mail_gun\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@Mail_Gun</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>\n```\n\nNow we can easily modify this file to suit our needs. Here are the changes I'll make:\n\n```diff title=\"templates/email/action.html\"\n <head>\n <meta name=\"viewport\" content=\"width=device-width\" />\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n-<title>Actionable emails e.g. reset password</title>\n+<title>Welcome to Stores REST API</title>\n \n \n <style type=\"text/css\">\n@@ -64,22 +64,22 @@ background-color: #f6f6f6;\n \t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n \t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n \t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n-\t\t\t\t\t\t\t\t\t\tPlease confirm your email address by clicking the link below.\n+\t\t\t\t\t\t\t\t\t\tWelcome to the Stores REST API.\n \t\t\t\t\t\t\t\t\t</td>\n \t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n-\t\t\t\t\t\t\t\t\t\tWe may need to send you critical information about our service and it is important that we have an accurate email address.\n+\t\t\t\t\t\t\t\t\t\tYour account has been created successfully.\n \t\t\t\t\t\t\t\t\t</td>\n \t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n-\t\t\t\t\t\t\t\t\t\t<a href=\"http://www.mailgun.com\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Confirm email address</a>\n+\t\t\t\t\t\t\t\t\t\t<a href=\"http://127.0.0.1:5000/swagger-ui\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Visit API Documentation</a>\n \t\t\t\t\t\t\t\t\t</td>\n \t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n-\t\t\t\t\t\t\t\t\t\t&mdash; The Mailgunners\n+\t\t\t\t\t\t\t\t\t\t&mdash; Stores REST API\n \t\t\t\t\t\t\t\t\t</td>\n \t\t\t\t\t\t\t\t</tr></table></td>\n \t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n-\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/mail_gun\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@Mail_Gun</a> on Twitter.</td>\n+\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/jslvtr\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@jslvtr</a> on Twitter.</td>\n \t\t\t\t\t\t</tr></table></div></div>\n \t\t</td>\n \t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n \t</tr></table></body>\n-</html>\n+</html>\n```\n\n:::tip\nInstead of sending users to 127.0.0.1:5000, make sure to use your Render.com deployed URL!\n:::\n\nThis is good, and it'll look nice already! But we can do better. Let's personalize the email a bit so that users see what username they signed up with.\n\nTo do this personalization, I'll use the Jinja templating language, since it gets installed alongside Flask.\n\n```diff title=\"templates/email/action.html\"\n    </tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n-   We may need to send you critical information about our service and it is important that we have an accurate email address.\n+   Your account with username {{ username }} has been created successfully.\n        </td>\n    </tr>\n```\n\n## Rendering our template before sending the email\n\nNow that we've written our HTML template, we need to pass it through the Jinja templating language, to replace that `{{ username }}` with the actual user's username.\n\nLet's do this in `tasks.py`.\n\nFirst let's import `jinja2` and set it up so it can load template files from the `templates` folder (new code is highlighted):\n\n```py title=\"tasks.py\"\n...\n\n# highlight-start\nimport jinja2\n# highlight-end\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n# highlight-start\ntemplate_loader = jinja2.FileSystemLoader(\"templates\")\ntemplate_env = jinja2.Environment(loader=template_loader)\n\n\ndef render_template(template_filename, **context):\n    return template_env.get_template(template_filename).render(**context)\n# highlight-end\n\n...\n```\n\nThen let's modify our `send_simple_message` function so that it accepts an html body as well as text:\n\n```py title=\"tasks.py\"\n# highlight-start\ndef send_simple_message(to, subject, body, html):\n# highlight-end\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Jose Salvatierra <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n            # highlight-start\n            \"html\": html,\n            # highlight-end\n        },\n    )\n```\n\nAnd finally, let's pass in the rendered template when we call this function:\n\n```py title=\"tasks.py\"\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n        # highlight-start\n        render_template(\"email/action.html\", username=username),\n        # highlight-end\n    )\n```\n\nNow remember to restart your app _and_ your background worker, then try to register a user (you may have to delete the existing user first so you don't get duplicate emails).\n\nYou should get the HTML email delivered!\n\n![HTML email with button to go to the API documentation](https://res.cloudinary.com/teclado/image/upload/v1689180798/courses/rest-apis-flask-python/html-email_dyxasi.png)\n\n:::info Spam or junk?\nIf your emails are going to spam, it's likely because you're using the sandbox domain. When you use an actual domain, you have to go through various [domain verification steps](https://help.mailgun.com/hc/en-us/articles/360011702394-Why-Do-My-Emails-Go-to-Spam-) which reduce the likelihood of your emails ending up in spam.\n:::\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/.python-version",
    "content": "3.10.6\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\nimport jinja2\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\ntemplate_loader = jinja2.FileSystemLoader(\"templates\")\ntemplate_env = jinja2.Environment(loader=template_loader)\n\n\ndef render_template(template_filename, **context):\n    return template_env.get_template(template_filename).render(**context)\n\n\ndef send_simple_message(to, subject, body, html):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Jose Salvatierra <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n            \"html\": html,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n        render_template(\"email/action.html\", username=username),\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/templates/email/action.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Welcome to Stores REST API</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWelcome to the Stores REST API.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tYour account with username {{ username }} has been created successfully.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://127.0.0.1:5000/swagger-ui\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Visit API Documentation</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; Stores REST API\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/jslvtr\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@jslvtr</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/end/templates/email/action.original.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Actionable emails e.g. reset password</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tPlease confirm your email address by clicking the link below.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWe may need to send you critical information about our service and it is important that we have an accurate email address.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://www.mailgun.com\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Confirm email address</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; The Mailgunners\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/mail_gun\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@Mail_Gun</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/06_sending_html_emails/start/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\n\n\ndef send_simple_message(to, subject, body):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Your Name <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/README.md",
    "content": "---\nctslug: deploy-the-rq-background-worker-to-render-com\n---\n\n# Deploy the rq background worker to Render.com\n\nWhen deploying to Render.com, it's much easier if we don't have to pass the `REDIS_URL` and the queue name directly to the command.\n\nSo instead, let's create a `settings.py` file and put our `rq` worker configuration there:\n\n```python title=\"settings.py\"\nimport os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nREDIS_URL = os.getenv(\"REDIS_URL\", \"redis://localhost:6379\")\nQUEUES = [\"emails\", \"default\"]\n```\n\nThe names of the variables are important, see [the documentation](https://python-rq.org/docs/workers/#using-a-config-file) for all the options that are currently supported.\n\nTo run the `rq` worker using this settings file use `rq worker -c settings`.\n\nLet's add this to our repo, and then deploy the background worker to Render.com.\n\nFirst create a new background worker:\n\n![Create a new service of type background worker in Render.com](https://res.cloudinary.com/teclado/image/upload/v1689180804/courses/rest-apis-flask-python/render-create-bg-worker_agtqej.png)\n\nThen, give it a name and fill in its basic settings. The default works for the most part. Make sure it's in the same region as or close to your Postgres and Redis databases:\n\n![Filling in the Render basic worker information with its name set to 'rest-api-background-worker', environment set to 'docker', and region set to 'Frankfurt'](https://res.cloudinary.com/teclado/image/upload/v1689180803/courses/rest-apis-flask-python/render-bg-worker-basic-settings_tu8vkz.png)\n\nAdd the environment variables it needs. Although in this case it doesn't need the `DATABASE_URL`, you can add it if you will be adding other tasks that do use the database in the near future. If not, leave it out.\n\n:::warning Internal URL\nIf your Redis database is with Render.com, you'd want to use the Redis database **Internal URL**, but I encountered some issues with it where the `redis` package didn't recognise the URL. Try it, but fall back to the external URL if it doesn't work.\n:::\n\n![Environment variables added in Render.com including DATABASE_URL, REDIS_URL, MAILGUN_API_KEY, and MAILGUN_DOMAIN, with their respective values](https://res.cloudinary.com/teclado/image/upload/v1689180803/courses/rest-apis-flask-python/render-bg-worker-env-vars_zmgmwp.png)\n\nFinally, this \"background worker\" is just a Python program without networking capabilities. So if we leave it as is, it will actually just run our Dockerfile and the Dockerfile's `CMD` command (which starts our web application). Therefore we want to give it a custom Docker command that starts the background worker.\n\nIn that command, I'll go into the `/app` directory of the Docker container, and run the `rq` worker passing in the `settings.py` file.\n\nThe command is `/bin/bash -c cd /app && rq worker -c settings`.\n\nThis is what it looks like in Render.com:\n\n![Screenshot showing the Docker command in Render.com](https://res.cloudinary.com/teclado/image/upload/v1689180803/courses/rest-apis-flask-python/render-bg-worker-docker-command_jezho0.png)"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/.python-version",
    "content": "3.10.6\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/settings.py",
    "content": "import os\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\nREDIS_URL = os.getenv(\"REDIS_URL\", \"redis://localhost:6379\")\nQUEUES = [\"emails\", \"default\"]\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\nimport jinja2\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\ntemplate_loader = jinja2.FileSystemLoader(\"templates\")\ntemplate_env = jinja2.Environment(loader=template_loader)\n\n\ndef render_template(template_filename, **context):\n    return template_env.get_template(template_filename).render(**context)\n\n\ndef send_simple_message(to, subject, body, html):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Jose Salvatierra <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n            \"html\": html,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n        render_template(\"email/action.html\", username=username),\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/templates/email/action.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Welcome to Stores REST API</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWelcome to the Stores REST API.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tYour account with username {{ username }} has been created successfully.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://127.0.0.1:5000/swagger-ui\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Visit API Documentation</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; Stores REST API\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/jslvtr\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@jslvtr</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/templates/email/action.original.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Actionable emails e.g. reset password</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tPlease confirm your email address by clicking the link below.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWe may need to send you critical information about our service and it is important that we have an accurate email address.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://www.mailgun.com\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Confirm email address</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; The Mailgunners\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/mail_gun\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@Mail_Gun</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/.gitignore",
    "content": ".env\n.venv\n.vscode\n__pycache__\ndata.db\n*.pyc\n.DS_Store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/.python-version",
    "content": "3.10.6\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/Dockerfile",
    "content": "FROM python:3.10\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"/bin/bash\", \"docker-entrypoint.sh\"]"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/README.md",
    "content": "# REST APIs Recording Project\n\nNothing here yet!\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/app.py",
    "content": "import os\n\nfrom flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\nfrom flask_migrate import Migrate\nfrom dotenv import load_dotenv\n\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\nfrom resources.user import blp as UserBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    load_dotenv()\n\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or os.getenv(\"DATABASE_URL\", \"sqlite:///data.db\")\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    app.config[\"JWT_SECRET_KEY\"] = \"jose\"\n    jwt = JWTManager(app)\n\n    @jwt.token_in_blocklist_loader\n    def check_if_token_in_blocklist(jwt_header, jwt_payload):\n        return jwt_payload[\"jti\"] in BLOCKLIST\n\n    @jwt.revoked_token_loader\n    def revoked_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n            ),\n            401,\n        )\n    \n    @jwt.needs_fresh_token_loader\n    def token_not_fresh_callback(jwt_header, jwt_payload):\n        return (\n            jsonify(\n                {\n                    \"description\": \"The token is not fresh.\",\n                    \"error\": \"fresh_token_required\",\n                }\n            ),\n            401,\n        )\n\n    @jwt.additional_claims_loader\n    def add_claims_to_jwt(identity):\n        # Look in the database and see whether the user is an admin\n        if identity == 1:\n            return {\"is_admin\": True}\n        return {\"is_admin\": False}\n\n    @jwt.expired_token_loader\n    def expired_token_callback(jwt_header, jwt_payload):\n        return (\n            jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}),\n            401,\n        )\n\n    @jwt.invalid_token_loader\n    def invalid_token_callback(error):\n        return (\n            jsonify(\n                {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n            ),\n            401,\n        )\n\n    @jwt.unauthorized_loader\n    def missing_token_callback(error):\n        return (\n            jsonify(\n                {\n                    \"description\": \"Request does not contain an access token.\",\n                    \"error\": \"authorization_required\",\n                }\n            ),\n            401,\n        )\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n    api.register_blueprint(UserBlueprint)\n\n    return app"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/blocklist.py",
    "content": "\"\"\"\nblocklist.py\n\nThis file just contains the blocklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blocklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nflask db upgrade\n\nexec gunicorn --bind 0.0.0.0:80 \"app:create_app()\""
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url,\n        target_metadata=target_metadata,\n        literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/versions/07006e31e788_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 07006e31e788\nRevises: \nCreate Date: 2022-08-15 12:44:59.705694\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '07006e31e788'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=80), nullable=False),\n    sa.Column('password', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('username')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('users')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/versions/8ca023a4a4b0_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 8ca023a4a4b0\nRevises: 07006e31e788\nCreate Date: 2022-08-15 12:52:41.303543\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '8ca023a4a4b0'\ndown_revision = '07006e31e788'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/versions/bb5da1e68550_.py",
    "content": "\"\"\"empty message\n\nRevision ID: bb5da1e68550\nRevises: 8ca023a4a4b0\nCreate Date: 2022-08-29 13:06:57.697368\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'bb5da1e68550'\ndown_revision = '8ca023a4a4b0'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('items', 'price',\n               existing_type=sa.REAL(),\n               type_=sa.Float(precision=2),\n               existing_nullable=False)\n    op.alter_column('users', 'password',\n               existing_type=sa.VARCHAR(length=80),\n               type_=sa.String(length=256),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column('users', 'password',\n               existing_type=sa.String(length=256),\n               type_=sa.VARCHAR(length=80),\n               existing_nullable=False)\n    op.alter_column('items', 'price',\n               existing_type=sa.Float(precision=2),\n               type_=sa.REAL(),\n               existing_nullable=False)\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/versions/d8e0f80631fb_.py",
    "content": "\"\"\"empty message\n\nRevision ID: d8e0f80631fb\nRevises: bb5da1e68550\nCreate Date: 2022-10-11 14:46:28.100282\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"d8e0f80631fb\"\ndown_revision = \"bb5da1e68550\"\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column(\"users\", sa.Column(\"email\", sa.String(), nullable=False))\n    op.create_unique_constraint(\"email\", \"users\", [\"email\"])\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_constraint(\"email\", \"users\", type_=\"unique\")\n    op.drop_column(\"users\", \"email\")\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/__init__.py",
    "content": "from models.store import StoreModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.item_tags import ItemTags\nfrom models.user import UserModel"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/item.py",
    "content": "from sqlalchemy import ForeignKey\nfrom db import db\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False)\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = \"users\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    email = db.Column(db.String, unique=True, nullable=False)\n    password = db.Column(db.String(256), nullable=False)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/requirements.txt",
    "content": "flask==2.3.2\nflask-smorest\npython-dotenv\nsqlalchemy\nflask-sqlalchemy\nflask-jwt-extended\npasslib\nflask-migrate\ngunicorn\npsycopg2\nrequests\nredis\nrq"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<int:item_id>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    @jwt_required()\n    def delete(self, item_id):\n        jwt = get_jwt()\n        if not jwt.get(\"is_admin\"):\n            abort(401, message=\"Admin privilege required.\")\n            \n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n        \n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred whilte inserting the item.\")\n\n        return item"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/resources/store.py",
    "content": "import uuid\nfrom flask import request\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<int:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(200, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<int:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e)\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<int:item_id>/tag/<int:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n        \n        return tag\n    \n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<int:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n    \n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"}\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\"\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",\n        )"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/resources/user.py",
    "content": "import os\nimport redis\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom passlib.hash import pbkdf2_sha256\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    jwt_required,\n    get_jwt,\n)\nfrom rq import Queue\nfrom sqlalchemy import or_\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom models import UserModel\nfrom schemas import UserSchema, UserRegisterSchema\nfrom tasks import send_user_registration_email\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\nconnection = redis.from_url(\n    os.getenv(\"REDIS_URL\")\n)  # Get this from Render.com or run in Docker\nqueue = Queue(\"emails\", connection=connection)\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserRegisterSchema)\n    def post(self, user_data):\n        if UserModel.query.filter(\n            or_(\n                UserModel.username == user_data[\"username\"],\n                UserModel.email == user_data[\"email\"],\n            )\n        ).first():\n            abort(409, message=\"A user with that username or email already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            email=user_data[\"email\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        db.session.add(user)\n        db.session.commit()\n\n        queue.enqueue(send_user_registration_email, user.email, user.username)\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.query.filter(\n            UserModel.username == user_data[\"username\"]\n        ).first()\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(identity=str(user.id))\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out.\"}\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.query.get_or_404(user_id)\n        db.session.delete(user)\n        db.session.commit()\n        return {\"message\": \"User deleted.\"}, 200\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n    store_id = fields.Int()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int(dump_only=True)\n    username = fields.Str(required=True)\n    password = fields.Str(required=True)\n\n\nclass UserRegisterSchema(UserSchema):\n    email = fields.Str(required=True)\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/tasks.py",
    "content": "import os\nimport requests\nfrom dotenv import load_dotenv\nimport jinja2\n\nload_dotenv()\n\nDOMAIN = os.getenv(\"MAILGUN_DOMAIN\")\ntemplate_loader = jinja2.FileSystemLoader(\"templates\")\ntemplate_env = jinja2.Environment(loader=template_loader)\n\n\ndef render_template(template_filename, **context):\n    return template_env.get_template(template_filename).render(**context)\n\n\ndef send_simple_message(to, subject, body, html):\n    return requests.post(\n        f\"https://api.mailgun.net/v3/{DOMAIN}/messages\",\n        auth=(\"api\", os.getenv(\"MAILGUN_API_KEY\")),\n        data={\n            \"from\": f\"Jose Salvatierra <mailgun@{DOMAIN}>\",\n            \"to\": [to],\n            \"subject\": subject,\n            \"text\": body,\n            \"html\": html,\n        },\n    )\n\n\ndef send_user_registration_email(email, username):\n    return send_simple_message(\n        email,\n        \"Successfully signed up\",\n        f\"Hi {username}! You have successfully signed up to the Stores REST API.\",\n        render_template(\"email/action.html\", username=username),\n    )\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/templates/email/action.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Welcome to Stores REST API</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWelcome to the Stores REST API.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tYour account with username {{ username }} has been created successfully.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://127.0.0.1:5000/swagger-ui\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Visit API Documentation</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; Stores REST API\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/jslvtr\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@jslvtr</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>\n"
  },
  {
    "path": "docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/templates/email/action.original.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\">\n<head>\n<meta name=\"viewport\" content=\"width=device-width\" />\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n<title>Actionable emails e.g. reset password</title>\n\n\n<style type=\"text/css\">\nimg {\nmax-width: 100%;\n}\nbody {\n-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;\n}\nbody {\nbackground-color: #f6f6f6;\n}\n@media only screen and (max-width: 640px) {\n  body {\n    padding: 0 !important;\n  }\n  h1 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h2 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h3 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h4 {\n    font-weight: 800 !important; margin: 20px 0 5px !important;\n  }\n  h1 {\n    font-size: 22px !important;\n  }\n  h2 {\n    font-size: 18px !important;\n  }\n  h3 {\n    font-size: 16px !important;\n  }\n  .container {\n    padding: 0 !important; width: 100% !important;\n  }\n  .content {\n    padding: 0 !important;\n  }\n  .content-wrap {\n    padding: 10px !important;\n  }\n  .invoice {\n    width: 100% !important;\n  }\n}\n</style>\n</head>\n\n<body itemscope itemtype=\"http://schema.org/EmailMessage\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\">\n\n<table class=\"body-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;\" bgcolor=\"#f6f6f6\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t\t<td class=\"container\" width=\"600\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;\" valign=\"top\">\n\t\t\t<div class=\"content\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;\">\n\t\t\t\t<table class=\"main\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;\" bgcolor=\"#fff\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-wrap\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t<meta itemprop=\"name\" content=\"Confirm Email\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\" /><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tPlease confirm your email address by clicking the link below.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\tWe may need to send you critical information about our service and it is important that we have an accurate email address.\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t<a href=\"http://www.mailgun.com\" class=\"btn-primary\" itemprop=\"url\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;\">Confirm email address</a>\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;\" valign=\"top\">\n\t\t\t\t\t\t\t\t\t\t&mdash; The Mailgunners\n\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t</tr></table></td>\n\t\t\t\t\t</tr></table><div class=\"footer\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;\">\n\t\t\t\t\t<table width=\"100%\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><tr style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;\"><td class=\"aligncenter content-block\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;\" align=\"center\" valign=\"top\">Follow <a href=\"http://twitter.com/mail_gun\" style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;\">@Mail_Gun</a> on Twitter.</td>\n\t\t\t\t\t\t</tr></table></div></div>\n\t\t</td>\n\t\t<td style=\"font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;\" valign=\"top\"></td>\n\t</tr></table></body>\n</html>"
  },
  {
    "path": "docs/docs/12_task_queues_emails/Insomnia_section12.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-18T22:12:58.901Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_349d1cc5283848fb92a4911bd38bebc6\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491718,\"created\":1668809491718,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491715,\"created\":1668809491715,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"parentId\":null,\"modified\":1668809491665,\"created\":1668809491665,\"name\":\"Section 12\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_94b040afec0a43e098320c3b03d5f0d7\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491725,\"created\":1668809491725,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ea98179fc95645199746896edbe91c33\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809520188,\"created\":1668809491716,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"email\\\": \\\"example@gmail.com\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_82977ddde2e14e7c8ca337842483a5f2\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491722,\"created\":1668809491722,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d0cb7e79bbfd4e97a88ef48ab0ddab14\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491723,\"created\":1668809491723,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_88445b44c1074ac091314fa911a6e0cf\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491720,\"created\":1668809491720,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_23333e08fd834798b6cb5ae3fb9a4831\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491705,\"created\":1668809491705,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491701,\"created\":1668809491701,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_fbda11a32a194f8986c6a09bf6368f71\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491703,\"created\":1668809491703,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4cf6d863192347d2beb9738fffc06aa6\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491708,\"created\":1668809491708,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1f08a251169c46e8bbe9aa3b9ef34069\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491706,\"created\":1668809491706,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fed862a14dfb4fbca5c16a361e9337b2\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491710,\"created\":1668809491710,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_2ebdc85b06354c5c8a2ac2a12b9f74d6\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491711,\"created\":1668809491711,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_84fcf23f2c94480db2771b7fdd981998\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491679,\"created\":1668809491679,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491678,\"created\":1668809491678,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"req_290cb9eb4d684279aa9735f946009e32\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491683,\"created\":1668809491683,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e691135e8b7840a188b9b8a6ffee6082\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491681,\"created\":1668809491681,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4b8b6a31400046d9af34c3420bcb5f1d\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491685,\"created\":1668809491685,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_eea8d6e13fcb44ca99f683f25e9d7e9f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491694,\"created\":1668809491694,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_23d1586241d44ecda90d772f973ee792\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491689,\"created\":1668809491689,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_732f07562b5849b6ae03ee7a327d34d1\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491699,\"created\":1668809491699,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e67af8716d3e46849f0632c6f9cc845f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491690,\"created\":1668809491690,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0e44aa1615284e2b9469a5129ba11b9f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491692,\"created\":1668809491692,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ce38744e96c24a63ac7cb0b14cfb72ec\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491697,\"created\":1668809491697,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_05389734f73848a3922232b339a3d34d\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491667,\"created\":1668809491667,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_a60cc8f7a1dc4acfa31828ea71765d5e\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491673,\"created\":1668809491673,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_b684ea4fa67c4d13befbc1e6ee30fdb6\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491739,\"created\":1668809491675,\"fileName\":\"Section 12\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docs/12_task_queues_emails/_category_.json",
    "content": "{\n    \"label\": \"Task queues with rq and e-mail sending\",\n    \"position\": 12\n}\n"
  },
  {
    "path": "docs/docs/Insomnia_all_sections.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-11-18T22:13:36.391Z\",\"__export_source\":\"insomnia.desktop.app:v2022.6.0\",\"resources\":[{\"_id\":\"req_9451df3aae714e93a8ed529b3a1f99c2\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666124555354,\"created\":1666122990495,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423031,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666124528874,\"created\":1666124528874,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528874,\"_type\":\"request_group\"},{\"_id\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"parentId\":null,\"modified\":1666991857781,\"created\":1666122928011,\"name\":\"Section 3\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_0a9c4822679b4eae92dec7432fe144b8\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900810115,\"created\":1666123651275,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store3\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124422881,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e15dafc098ac4a2198304d2aead2a5b9\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900815265,\"created\":1666123912423,\"url\":\"http://127.0.0.1:5000/store/My Store/item\",\"name\":\"/store/<name>/item Create item in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Table\\\",\\n\\t\\\"price\\\": 17.99\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423081,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6363c8d4deb74b5bbccb1e2105277dac\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900812784,\"created\":1666124168137,\"url\":\"http://127.0.0.1:5000/store/My store3\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124422956,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_697ca0714a3d4e94819411e3df0a2a17\",\"parentId\":\"fld_8b9c03412d0e463fabe784d205f1d604\",\"modified\":1666900846590,\"created\":1666124316888,\"url\":\"http://127.0.0.1:5000/store/My store3/item\",\"name\":\"/store/<name>/item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423056,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_3d2b5cd58a4b4a6983c133118c5f8027\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666125193227,\"created\":1666124761134,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_afac4dd2683746c586c6ff61228611de\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666125229064,\"created\":1666124761133,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"parentId\":null,\"modified\":1666991873213,\"created\":1666124761123,\"name\":\"Section 5 before Docker\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_b9dafd45675e4c478fa4dd125f4827b3\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902941803,\"created\":1666124761136,\"url\":\"http://127.0.0.1:5000/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_bd3ecff11e5b49baa489812528235afb\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902781180,\"created\":1666124761139,\"url\":\"http://127.0.0.1:5000/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_34cbd59313d44bbfa4fd70166e341b05\",\"parentId\":\"fld_afac4dd2683746c586c6ff61228611de\",\"modified\":1666902749338,\"created\":1666124977832,\"url\":\"http://127.0.0.1:5000/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d48cf679c2664c9bb566b600634b966f\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902939274,\"created\":1666124761145,\"url\":\"http://127.0.0.1:5000/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": \\\"f48f94a4760e40d39debf155396a9dec\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666125224286,\"created\":1666124761144,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_8982d9bcce734f60a9f27a8eb1fc748c\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666125332019,\"created\":1666124928966,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_637d0fb6ba9d4c25b6ad9f5bdda73036\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902961406,\"created\":1666125038450,\"url\":\"http://127.0.0.1:5000/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_99fbb0c34cd049f1bb8ac4e944f0ae6d\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666902838552,\"created\":1666125104208,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e581f2420345418c84d71dbed226b6da\",\"parentId\":\"fld_91ec9103821245f69f82aa78362f81e1\",\"modified\":1666125710431,\"created\":1666125184534,\"url\":\"http://127.0.0.1:5000/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_08302ba35f784bdc9fa2edc0cb080287\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985452213,\"created\":1666905719010,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719008,\"created\":1666905719008,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"parentId\":null,\"modified\":1666991880304,\"created\":1666905718998,\"name\":\"Section 5 - Docker\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_0c240b23280746a6a1a56d7644fb89ce\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666987464108,\"created\":1666905719011,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6fdedbe47a9941af9b8459816f179274\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985886605,\"created\":1666905719013,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fc255f6789fe45ed80b2ef83e6bb6645\",\"parentId\":\"fld_0bc4d91251f54e1d8e00966a259b35bc\",\"modified\":1666985462540,\"created\":1666905719014,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_3d189bf5d88349e3bce363a420407f65\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666987468265,\"created\":1666905719018,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": \\\"8efca659f8674c56b5cd035ecc0d42ec\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_80dee5df10c347198d8f12d85703d582\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719016,\"created\":1666905719016,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_35d865c76bce4e1b9c378d82ece413f7\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666985474126,\"created\":1666905719019,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b42e3c4d855a433394ac1a8a60c2b91b\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666985467338,\"created\":1666905719020,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_c2bf495d5cbb49d8b933b832a717662a\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666987071504,\"created\":1666905719022,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9a89b2ecfc61457d8cac15985597c0a0\",\"parentId\":\"fld_80dee5df10c347198d8f12d85703d582\",\"modified\":1666986841489,\"created\":1666905719023,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8612530e54144a039af84006ee8c882d\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689179,\"created\":1666987689179,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689178,\"created\":1666987689178,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"parentId\":null,\"modified\":1666987689171,\"created\":1666987689171,\"name\":\"Section 6\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_4f7b9d616b0e44ca94ca51cc71660da0\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666990320166,\"created\":1666987689181,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_335002433e9745068d074f1f942ddde2\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689183,\"created\":1666987689183,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9228903cf7a54601a51a59f6a6692363\",\"parentId\":\"fld_7ed8d16fd87545519f2f64b2613ea84a\",\"modified\":1666987689184,\"created\":1666987689184,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0624b67ef6b841f482b7e7522fb6f405\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666990328367,\"created\":1666987689187,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689186,\"created\":1666987689186,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_b2651043ea5e4b33b073f260712fb114\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689189,\"created\":1666987689189,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d1d499ead63e469ca04571899cc4759f\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689190,\"created\":1666987689190,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_640f313dbd5a4bfcbf98081e2fab6d4a\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689192,\"created\":1666987689192,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_94738c7e8c774bd597ffe97bf7b921b6\",\"parentId\":\"fld_8761c7b0aa5142cba8985868cbda3de2\",\"modified\":1666987689194,\"created\":1666987689194,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ab3c728a796e4b4ca51803248e1b0650\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745596,\"created\":1666990745596,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_597937a09435404ebe2200cbaeed101d\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745596,\"created\":1666990745596,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"parentId\":null,\"modified\":1666990745588,\"created\":1666990745588,\"name\":\"Section 7\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_8a36225a08bb4dfbbf98fd983b0d4a5f\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666991654175,\"created\":1666990745599,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store2\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_a9d43bb23e1246da94aec50b9b9ca652\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745601,\"created\":1666990745601,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_303507538c0f408eb6d91784b7ed8d36\",\"parentId\":\"fld_597937a09435404ebe2200cbaeed101d\",\"modified\":1666990745602,\"created\":1666990745602,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6c72c92f81924ce7bc26ceb488fd64ff\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666991658886,\"created\":1666990745605,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chair\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745604,\"created\":1666990745604,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_e86e0877045640d690454a99b176f3a2\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745607,\"created\":1666990745607,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f4776751aecc4c6eafb264dc2d2c24cb\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745609,\"created\":1666990745609,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f0c4a3d747a543249131e19ceea79e56\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745610,\"created\":1666990745610,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e6bc2422c8cf4f119c7dc10251a9af65\",\"parentId\":\"fld_baa111a1ff5849b4838637f09844bfde\",\"modified\":1666990745611,\"created\":1666990745611,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_85adfd198935497bb7aedb266beb5bf3\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991788350,\"created\":1666990945502,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_86b5e8072a894c409febe46716e99809\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990939045,\"created\":1666990939045,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_379d0e42420f466bbad1b7481e5e7816\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991794866,\"created\":1666990973919,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f07aab6ead044ca7bba0de3437ab08c4\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991779049,\"created\":1666991031108,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4765f7ca8e1e46308cdde255d09a2ffc\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991810641,\"created\":1666991378432,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_77d1a5f225c54acbb27bac15010722ad\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991824192,\"created\":1666991489163,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d60510ab22b2499abb20a63629e30fcd\",\"parentId\":\"fld_86b5e8072a894c409febe46716e99809\",\"modified\":1666991828682,\"created\":1666991524256,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_2ba32c3564f3456aa1c8731323508968\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1666991842400,\"created\":1666991842400,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842399,\"created\":1666991842399,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"parentId\":null,\"modified\":1666991842388,\"created\":1666991842388,\"name\":\"Section 8\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_57daa411077044a98d3b2534ee735703\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1667332324728,\"created\":1666991842402,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_047bf72b98214de4a8f711fd46b73eb4\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1666991842403,\"created\":1666991842403,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6c1af0972dbe4faebf5973d95670c241\",\"parentId\":\"fld_a18660fe822f44828b330ba9a4d3946a\",\"modified\":1667332341585,\"created\":1666991842405,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_32c5d69aec8f44fdb33852b456c99b7a\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332259971,\"created\":1666991842408,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842407,\"created\":1666991842407,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_1843f2db017842b993251abbedeb2e8b\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332298429,\"created\":1666991842410,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0d1c09067c6a419a9096b32da16d01a3\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332272845,\"created\":1666991842412,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d311f219368d46a6b6d92be142a32763\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332311745,\"created\":1666991842413,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_dbdfc5218bf54bfab39db152b3ffe982\",\"parentId\":\"fld_b2b78ef1cd504663bef29f23cb3fd9a7\",\"modified\":1667332288041,\"created\":1666991842414,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjA3MCwianRpIjoiODA5Njk2MmUtYmNlMy00NjgyLWJkZTUtYjYzMDgwZGU3NmJjIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyMDcwLCJleHAiOjE2NjczMzI5NzB9.MY-MKMh8mEaX8cWjz0RJZ2JStD7hHBD1j7nB3GT2vks\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_f613fc18d27648daa00d6b78deea5b66\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842418,\"created\":1666991842418,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842417,\"created\":1666991842417,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_362ff303b1054bd0b0d62522803aea64\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842419,\"created\":1666991842419,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_266cac65472a4a118929460e58893fb0\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842421,\"created\":1666991842421,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e899763b1c0a46ad9eb47f9e628aa643\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842422,\"created\":1666991842422,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e63e0532c2c340aa8ed6643f0ae1b4ec\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842424,\"created\":1666991842424,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5c6887cfb9c94dc2aa2fe76d525fecac\",\"parentId\":\"fld_815efb55c04548dea5dfec2e2f69ebcb\",\"modified\":1666991842426,\"created\":1666991842426,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fe639dc55a2d439f9aa7a6bceee6a9fa\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332404117,\"created\":1667328597818,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1667328596182,\"created\":1667328596182,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"req_adb596c3d0ee48e2b009a555297f36ac\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667331428779,\"created\":1667328670383,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_953d7fdcaf1f433b9b3b0cdf6453b0cd\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332353929,\"created\":1667328704142,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5e7ca3df73314f8c839493d53528760b\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1667332406439,\"created\":1667331560707,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_bc66397203734df4b4eb6d153d8d3ec3\",\"parentId\":\"fld_3426c101e9094fa79fbf96cf1ec23fae\",\"modified\":1668007854634,\"created\":1667332459073,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_22eb90da9d974ac2880b9207d6d11c01\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566845,\"created\":1667332566845,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_6315121566934d098d5f963bb2b25679\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566844,\"created\":1667332566844,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"parentId\":null,\"modified\":1667332566837,\"created\":1667332566837,\"name\":\"Section 8 - Chaining\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_3efb83da0cb34711961a59b6b3b39278\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566848,\"created\":1667332566848,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_44b8b847e3bd4b35bb7c5d2df5e0ee94\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566849,\"created\":1667332566849,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_24669eae6530416094eb2c746ae577ed\",\"parentId\":\"fld_6315121566934d098d5f963bb2b25679\",\"modified\":1667332566851,\"created\":1667332566851,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_69cc615e910a4ca7a91382b455c3ceb5\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332840222,\"created\":1667332566854,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_68ade160b31f446995815fec3dc30a93\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566853,\"created\":1667332566853,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_310092462fe44643bdbcf671dd488033\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332838232,\"created\":1667332566855,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_41e02aac89e5447fb70b42a5fbb6e42d\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667650833785,\"created\":1667332566857,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_260395e8e71c47559f2c70f50618ea38\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332566858,\"created\":1667332566858,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_51ea1a9b34584746a8368ede6f6bfadf\",\"parentId\":\"fld_68ade160b31f446995815fec3dc30a93\",\"modified\":1667332848385,\"created\":1667332566859,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e2bc3c7dd1c240baad60b3881359bf38\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566862,\"created\":1667332566862,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566861,\"created\":1667332566861,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_ba56440f8f204525a13fbdfbd2273ae0\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566864,\"created\":1667332566864,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9237adf4f3e74e1b9aa8dbb15680c2f3\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566866,\"created\":1667332566866,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ae7151238fe84a65b5b1064b87cef5bd\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566867,\"created\":1667332566867,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_c11ab9d081754c3e95f0eed61219454a\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566868,\"created\":1667332566868,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_03468f66652a42dcbd34ef05ce4fd714\",\"parentId\":\"fld_db9477d576514920b5a9e1d2d82d2254\",\"modified\":1667332566870,\"created\":1667332566870,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_252ddd492b9242ffb1a6fe21e25534c5\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667650976611,\"created\":1667332566872,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566872,\"created\":1667332566872,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"req_00cda5fbc60a4905bf5b8e67aa7dadb4\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566874,\"created\":1667332566874,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_229bc7e2601e44cb82eb3e5eafa90202\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566875,\"created\":1667332566875,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fe8e836ebd4a44cd9f37ff8738e7ed98\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667650979775,\"created\":1667332566877,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_6a638b0dfd3040118a0282fe5a49b5fb\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667332566878,\"created\":1667332566878,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0ae766df1c25481e83bd6ade061a919b\",\"parentId\":\"fld_f0dbcc5050af40dbaabdd9bfe69a098f\",\"modified\":1667676377948,\"created\":1667650753174,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fe2a4b3d67664cd194efe09393b9fd02\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660660,\"created\":1668007660660,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660658,\"created\":1668007660658,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"parentId\":null,\"modified\":1668007660646,\"created\":1668007660646,\"name\":\"Section 11\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_90015fc3fcae4301b0d8a5c0355a3d83\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668678375918,\"created\":1668007660661,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b34295f6d8b24000b77f812c9cc2c2cc\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660663,\"created\":1668007660663,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0e20a55b16034f06a3246c4e76f1c387\",\"parentId\":\"fld_db95f405b201413889d2ae8918eee2d4\",\"modified\":1668007660665,\"created\":1668007660665,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b8cc399abc8749eea66c6967522f8a36\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668678378059,\"created\":1668007660669,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660668,\"created\":1668007660668,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_8e9e92854eab4e74af765725b54c7d31\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660671,\"created\":1668007660671,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_671694da515a4c4e877008b02cd17f57\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660673,\"created\":1668007660673,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5303d646b4c44843bc9c8322675f9c54\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668678378608,\"created\":1668007660675,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_a55050436885439f8393e70546d75641\",\"parentId\":\"fld_11fd3ae720264f0b9480c6277f1a7633\",\"modified\":1668007660676,\"created\":1668007660676,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_c063f1996fde409f81d62a0e3d20b631\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660681,\"created\":1668007660681,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660679,\"created\":1668007660679,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_8b7c05ec34874814bbb1ed17f23d7ea3\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660682,\"created\":1668007660682,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5739a9e1be2e4fcd8ecdac5142b5e76d\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668678381399,\"created\":1668007660684,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_007ed54990944618a9e4bbf4fed9bdc8\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660686,\"created\":1668007660686,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fbc4905447b04feca058e6f8c0428acf\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660688,\"created\":1668007660688,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_839409cd69e945b694d46f3101eec0d5\",\"parentId\":\"fld_3083883073354f11bb6c2aa2fcbe76d3\",\"modified\":1668007660690,\"created\":1668007660690,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_a9790bade2c14dd5a87b85088017aa92\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668809417998,\"created\":1668007660694,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660692,\"created\":1668007660692,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"req_92cec23efa0d47a582b4cf476fbe1c7d\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660695,\"created\":1668007660695,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_969a6a61d9b74bd3ad27623663999dca\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660697,\"created\":1668007660697,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_0aa8a2f045e94cea96ba0f7061a26c41\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668678383934,\"created\":1668007660698,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1a5f0c9aa22841e0a60e47c6a5f65edc\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668678384453,\"created\":1668007660701,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_70c9c31ae27e47b684428bb88932f96d\",\"parentId\":\"fld_8173221215364faa9eaf4de28d0b227d\",\"modified\":1668007660702,\"created\":1668007660702,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_84fcf23f2c94480db2771b7fdd981998\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491679,\"created\":1668809491679,\"url\":\"{{url}}/store\",\"name\":\"/store Get all store data\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423181,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491678,\"created\":1668809491678,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528974,\"_type\":\"request_group\"},{\"_id\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"parentId\":null,\"modified\":1668809491665,\"created\":1668809491665,\"name\":\"Section 12\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_e691135e8b7840a188b9b8a6ffee6082\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491681,\"created\":1668809491681,\"url\":\"{{url}}/store\",\"name\":\"/store Create new store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"My store22\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_2f351442a28746c98995c18ea907feaa\"}],\"authentication\":{},\"metaSortKey\":-1666124423143.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_290cb9eb4d684279aa9735f946009e32\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491683,\"created\":1668809491683,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423156,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4b8b6a31400046d9af34c3420bcb5f1d\",\"parentId\":\"fld_020cd4f64dbc4a31921e85c95a49d633\",\"modified\":1668809491685,\"created\":1668809491685,\"url\":\"{{url}}/store/STORE_ID\",\"name\":\"/store/<id> Delete store\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666124423131,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_e67af8716d3e46849f0632c6f9cc845f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491690,\"created\":1668809491690,\"url\":\"{{url}}/item\",\"name\":\"/item Create item\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Chairs\\\",\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 1\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_c60e217df8494bf1af1c02b872b5430a\"},{\"id\":\"pair_e6b6dbc1883d4ee1a90ac154e8fbb3a7\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104220.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_23d1586241d44ecda90d772f973ee792\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491689,\"created\":1668809491689,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666124528924,\"_type\":\"request_group\"},{\"_id\":\"req_0e44aa1615284e2b9469a5129ba11b9f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491692,\"created\":1668809491692,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Delete item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_1b62f1a1d23245439dfc1a356f67cd36\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104214.25,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_eea8d6e13fcb44ca99f683f25e9d7e9f\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491694,\"created\":1668809491694,\"url\":\"{{url}}/item\",\"name\":\"/item Get all items\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d42eaef671e847b2a2eadb8f3af9be22\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104308,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ce38744e96c24a63ac7cb0b14cfb72ec\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491697,\"created\":1668809491697,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Update item\",\"description\":\"\",\"method\":\"PUT\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Another Chair\\\",\\n\\t\\\"price\\\": 20.00\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_4c9b7b960e3b499f8e94f507537195ae\"}],\"authentication\":{},\"metaSortKey\":-1666125104208,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_732f07562b5849b6ae03ee7a327d34d1\",\"parentId\":\"fld_23d1586241d44ecda90d772f973ee792\",\"modified\":1668809491699,\"created\":1668809491699,\"url\":\"{{url}}/item/ITEM_ID\",\"name\":\"/item/<id> Get item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_117512e4f7834d24ab18754279464356\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1666125104258,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fbda11a32a194f8986c6a09bf6368f71\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491703,\"created\":1668809491703,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tags/<id> Get tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945502,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491701,\"created\":1668809491701,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1666990939045,\"_type\":\"request_group\"},{\"_id\":\"req_23333e08fd834798b6cb5ae3fb9a4831\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491705,\"created\":1668809491705,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Get tags in store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990973919,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1f08a251169c46e8bbe9aa3b9ef34069\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491706,\"created\":1668809491706,\"url\":\"{{url}}/store/STORE_ID/tag\",\"name\":\"/stores/<id>/tags Create tag in store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"name\\\": \\\"Tag name\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\"}],\"authentication\":{},\"metaSortKey\":-1666990945452,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_4cf6d863192347d2beb9738fffc06aa6\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491708,\"created\":1668809491708,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Link an item in a store with a tag from the same store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945477,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_fed862a14dfb4fbca5c16a361e9337b2\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491710,\"created\":1668809491710,\"url\":\"{{url}}/item/ITEM_ID/tag/TAG_ID\",\"name\":\"/item/<id>/tag/<id> Unlink a tag from an item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945427,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_2ebdc85b06354c5c8a2ac2a12b9f74d6\",\"parentId\":\"fld_a2feddc6ef5e44a495722d631fb86a4e\",\"modified\":1668809491711,\"created\":1668809491711,\"url\":\"{{url}}/tag/TAG_ID\",\"name\":\"/tag/<id> Delete a tag, which must have no associated items.\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1666990945402,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_ea98179fc95645199746896edbe91c33\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809520188,\"created\":1668809491716,\"url\":\"{{url}}/register\",\"name\":\"/register Create user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"email\\\": \\\"example@gmail.com\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_975ec20d9bec44538591da08c95ccd63\"}],\"authentication\":{},\"metaSortKey\":-1667328597818,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491715,\"created\":1668809491715,\"name\":\"Users\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1667328596182,\"_type\":\"request_group\"},{\"_id\":\"req_349d1cc5283848fb92a4911bd38bebc6\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491718,\"created\":1668809491718,\"url\":\"{{url}}/user/USER_ID\",\"name\":\"/user/<id> Get user\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328670383,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_88445b44c1074ac091314fa911a6e0cf\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491720,\"created\":1668809491720,\"url\":\"{{url}}/user/1\",\"name\":\"/user/<id> Delete user\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1667328597768,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_82977ddde2e14e7c8ca337842483a5f2\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491722,\"created\":1668809491722,\"url\":\"{{url}}/login\",\"name\":\"/login Authenticate user\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"}],\"authentication\":{},\"metaSortKey\":-1667328597793,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_d0cb7e79bbfd4e97a88ef48ab0ddab14\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491723,\"created\":1668809491723,\"url\":\"{{url}}/logout\",\"name\":\"/logout Revoke JWT\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"application/json\",\"text\":\"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_716a234feee94c3b9e79966a066fc00d\"},{\"id\":\"pair_c27fa3320a6943cfbf87c40b7ca6ab73\",\"name\":\"Authorization\",\"value\":\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2NzMzMjQwNiwianRpIjoiMTU2ZTc5MzUtMmYwYy00NDE2LThmYWItODExYzdmNjY0ZDk4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6MSwibmJmIjoxNjY3MzMyNDA2LCJleHAiOjE2NjczMzMzMDZ9.zht2vTD73oderxN4uBobsywovxHdDjohbDNGk8_Vuaw\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328597780.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_94b040afec0a43e098320c3b03d5f0d7\",\"parentId\":\"fld_86d10d221e114aa2be9542daf31009ec\",\"modified\":1668809491725,\"created\":1668809491725,\"url\":\"{{url}}/refresh\",\"name\":\"/refresh Get non-fresh token\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_d002eea6b22e4190909fc8366b5a4704\",\"name\":\"Authorization\",\"value\":\"Bearer {{refresh_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1667328634100.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_19db457230041d88ca9420d1b3c0f1f02bbcae93\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928025,\"created\":1666122928025,\"name\":\"Base Environment\",\"data\":{},\"dataPropertyOrder\":null,\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_19db457230041d88ca9420d1b3c0f1f02bbcae93\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928027,\"created\":1666122928027,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_c5b803a7c6514ff29573e26487d898d4\",\"parentId\":\"wrk_6aa19b7d9ecd4f93a3602d257e54a163\",\"modified\":1666122928018,\"created\":1666122928018,\"fileName\":\"Your First REST API\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_6b3e8bb38d0c4154826d63642b863687\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761125,\"created\":1666124761125,\"name\":\"Base Environment\",\"data\":{},\"dataPropertyOrder\":null,\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_9b95c15dadb44c03bf60cc7386095847\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761128,\"created\":1666124761128,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_cfb94f75feff4930966c80f350b1e115\",\"parentId\":\"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a\",\"modified\":1666124761155,\"created\":1666124761131,\"fileName\":\"Flask-Smorest\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_adf22718b4e044e5b54b37c869463582\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666985430514,\"created\":1666905719000,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_210b7ba8709f44f29c305ed544da17c3\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719004,\"created\":1666905719004,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_7a427f233a494727845a45ba1325ea85\",\"parentId\":\"wrk_e6c8aab80c134d35810fd37d43cce51e\",\"modified\":1666905719034,\"created\":1666905719007,\"fileName\":\"Flask-Smorest-Docker\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_892efa21f8454221972d0c77a336872c\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689172,\"created\":1666987689172,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_aff586a35c4c49aa91c5defb067355bf\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689174,\"created\":1666987689174,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_4e7424f78749436bacdb44d3a1eba77a\",\"parentId\":\"wrk_a6cd641e98494bca9a11fe77b66c7e37\",\"modified\":1666987689205,\"created\":1666987689176,\"fileName\":\"Section 6\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_7609e8f1315a4d77af52a6ba50f48205\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745590,\"created\":1666990745590,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_ce9759718e054191a685cec521ed7afc\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745592,\"created\":1666990745592,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_0f62897a05a449f9845b4c71eeb892b3\",\"parentId\":\"wrk_6efa5c8b8fa142a28f436b209fba66fa\",\"modified\":1666990745620,\"created\":1666990745594,\"fileName\":\"Section 7\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_52ad539864c4425884f6394f62627cb7\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1667329454452,\"created\":1666991842391,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\"},\"dataPropertyOrder\":{\"&\":[\"url\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_bc7c508b3eed4f2485782e9d7177762e\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842395,\"created\":1666991842395,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_75bd139c1688445faff87b21105f8df1\",\"parentId\":\"wrk_101d091b922e44c28f66528e9ef2ae37\",\"modified\":1666991842435,\"created\":1666991842397,\"fileName\":\"Section 8\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_c2a4191124a544e9a440f8042644c7b9\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667651225760,\"created\":1667332566839,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_8a4500f913d7401fa00168eaf32dcb4c\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566841,\"created\":1667332566841,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_2d4dfbfd115b4f91b44d09ad3c6d0b5b\",\"parentId\":\"wrk_5d43c6fa3d9a4ba8817aa2e2767d791a\",\"modified\":1667332566888,\"created\":1667332566842,\"fileName\":\"Section 8 - Chaining\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_e09818240d524d079150f6fc00963c54\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660648,\"created\":1668007660648,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_9aa71c77793345cabd413a66ad557bd7\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660652,\"created\":1668007660652,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_2f42797dc9c6441fb3d3e7b23780dc27\",\"parentId\":\"wrk_f14186e0f24a4da9ab3ab8d628227a36\",\"modified\":1668007660725,\"created\":1668007660654,\"fileName\":\"Section 11\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"},{\"_id\":\"env_05389734f73848a3922232b339a3d34d\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491667,\"created\":1668809491667,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5005\",\"access_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'when-expired', 300 %}\",\"refresh_token\":\"{% response 'body', 'req_fe8e836ebd4a44cd9f37ff8738e7ed98', 'b64::JC5yZWZyZXNoX3Rva2Vu::46b', 'when-expired', NaN %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\",\"refresh_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1666122928025,\"_type\":\"environment\"},{\"_id\":\"jar_a60cc8f7a1dc4acfa31828ea71765d5e\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491673,\"created\":1668809491673,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_b684ea4fa67c4d13befbc1e6ee30fdb6\",\"parentId\":\"wrk_3626a93c829b4cf683cbf49c61bdaaef\",\"modified\":1668809491739,\"created\":1668809491675,\"fileName\":\"Section 12\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "docs/docusaurus.config.js",
    "content": "// @ts-check\n// Note: type annotations allow type checking and IDEs autocompletion\n\nimport { Highlight, themes } from \"prism-react-renderer\";\n\n/** @type {import('@docusaurus/types').Config} */\nconst config = {\n  title: \"REST APIs with Flask and Python\",\n  tagline: \"Build and deploy REST APIs using Flask, PostgreSQL, and Docker\",\n  url: \"https://rest-apis-flask.teclado.com\",\n  baseUrl: \"/\",\n  onBrokenLinks: \"throw\",\n  onBrokenMarkdownLinks: \"warn\",\n  favicon: \"img/favicon.ico\",\n  organizationName: \"tecladocode\", // Usually your GitHub org/user name.\n  projectName: \"rest-apis-flask-python\", // Usually your repo name.\n  scripts: [\n    {\n      src: \"https://plau-prox.teclado.workers.dev/get/script.outbound-links.js\",\n      defer: true,\n      \"data-domain\": \"rest-apis-flask.teclado.com\",\n      \"data-api\": \"https://plau-prox.teclado.workers.dev/send/event\",\n    },\n  ],\n  presets: [\n    [\n      \"@docusaurus/preset-classic\",\n      /** @type {import('@docusaurus/preset-classic').Options} */\n      ({\n        docs: {\n          sidebarPath: require.resolve(\"./sidebars.js\"),\n          exclude: [\"**/start/**\", \"**/end/**\"],\n          // Please change this to your repo.\n          editUrl:\n            \"https://github.com/tecladocode/rest-apis-flask-python/tree/develop/docs/\",\n        },\n        theme: {\n          customCss: require.resolve(\"./src/css/custom.css\"),\n        },\n      }),\n    ],\n  ],\n  themeConfig:\n    /** @type {import('@docusaurus/preset-classic').ThemeConfig} */\n    ({\n      docs: {\n        sidebar: {\n          hideable: true,\n        },\n      },\n      algolia: {\n        // The application ID provided by Algolia\n        appId: \"1BEGBIP9SH\",\n\n        // Public API key: it is safe to commit it\n        apiKey: \"882167549d623413f9b5314788a0d900\",\n\n        indexName: \"docusaurus-2\",\n\n        // Optional: see doc section below\n        // contextualSearch: true,\n\n        // Optional: Specify domains where the navigation should occur through window.location instead on history.push. Useful when our Algolia config crawls multiple documentation sites and we want to navigate with window.location.href to them.\n        // externalUrlRegex: \"external\\\\.com|domain\\\\.com\",\n\n        // Optional: Algolia search parameters\n        searchParameters: {},\n\n        // Optional: path for search page that enabled by default (`false` to disable it)\n        searchPagePath: \"search\",\n      },\n      navbar: {\n        title: \"REST APIs with Flask and Python\",\n        logo: {\n          alt: \"Teclado Logo\",\n          src: \"img/favicon.ico\",\n        },\n        items: [\n          {\n            type: \"doc\",\n            docId: \"course_intro/intro\",\n            position: \"left\",\n            label: \"Tutorial\",\n          },\n          {\n            href: \"/insomnia-files/\",\n            position: \"left\",\n            label: \"Insomnia files\",\n          },\n          {\n            href: \"https://go.tecla.do/rest-apis-ebook\",\n            label: \"Get the course\",\n            position: \"right\",\n          },\n        ],\n      },\n      announcementBar: {\n        id: \"support_us\",\n        content:\n          '<span style=\"font-weight: 600\">Unlock all video lessons and support us by <a target=\"_blank\" style=\"background-color: #ff7d82; background-image: linear-gradient(90deg, #FF7D82, #50e3c2); background-clip: text; color: transparent; -webkit-background-clip: text; -moz-background-clip: text; -webkit-text-fill-color: transparent; -moz-text-fill-color: transparent; \" rel=\"noopener noreferrer\" href=\"https://go.tecla.do/rest-apis-ebook\">buying the course</a>!</span>',\n        backgroundColor: \"#1c2023\",\n        textColor: \"#fff\",\n        isCloseable: false,\n      },\n      footer: {\n        style: \"dark\",\n        links: [\n          {\n            title: \"Learn\",\n            items: [\n              {\n                href: \"https://go.tecla.do/rest-apis-ebook\",\n                label: \"Get the course\",\n              },\n              {\n                label: \"Tutorial\",\n                to: \"/docs/course_intro/\",\n              },\n            ],\n          },\n          {\n            title: \"Social\",\n            items: [\n              {\n                label: \"Discord\",\n                href: \"https://go.tecla.do/discord\",\n              },\n              {\n                label: \"Twitter\",\n                href: \"https://twitter.com/jslvtr\",\n              },\n            ],\n          },\n          {\n            title: \"More\",\n            items: [\n              {\n                label: \"GitHub\",\n                href: \"https://github.com/tecladocode/rest-apis-flask-python\",\n              },\n            ],\n          },\n        ],\n        copyright: `Copyright © ${new Date().getFullYear()} Teclado Ltd. Built with Docusaurus.`,\n      },\n      prism: {\n        theme: themes.okaidia,\n        darkTheme: themes.dracula,\n        additionalLanguages: [\"docker\"],\n      },\n    }),\n};\n\nexport default config;\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"website\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"3.2.0\",\n    \"@docusaurus/preset-classic\": \"3.2.0\",\n    \"@docusaurus/theme-search-algolia\": \"3.2.0\",\n    \"@mdx-js/react\": \"^3.0.1\",\n    \"clsx\": \"^2.1.0\",\n    \"prism-react-renderer\": \"^2.3.1\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  }\n}\n"
  },
  {
    "path": "docs/sidebars.js",
    "content": "/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that group\n - provide next/previous navigation\n\n The sidebars can be generated from the filesystem, or explicitly defined here.\n\n Create as many sidebars as you want.\n */\n\n// @ts-check\n\n/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */\nconst sidebars = {\n  // By default, Docusaurus generates a sidebar from the docs folder structure\n  tutorialSidebar: [{ type: \"autogenerated\", dirName: \".\" }],\n\n  // But you can create a sidebar manually\n  /*\n  tutorialSidebar: [\n    {\n      type: 'category',\n      label: 'Tutorial',\n      items: ['hello'],\n    },\n  ],\n   */\n};\n\nmodule.exports = sidebars;\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures/index.js",
    "content": "import React from \"react\";\nimport clsx from \"clsx\";\nimport styles from \"./styles.module.css\";\n\nconst FeatureList = [\n  {\n    title: \"Everything you need\",\n    Svg: require(\"@site/static/img/product-dev.svg\").default,\n    description: (\n      <>\n        Learn Flask, Docker, PostgreSQL, and more. Build professional-grade REST\n        APIs with Python.\n      </>\n    ),\n  },\n  {\n    title: \"The latest versions\",\n    Svg: require(\"@site/static/img/cloud-download.svg\").default,\n    description: (\n      <>\n        No more outdated tutorials. Use Python 3.10+ and the latest versions of\n        every Flask extension and library.\n      </>\n    ),\n  },\n  {\n    title: \"Use best practices\",\n    Svg: require(\"@site/static/img/robot-coding.svg\").default,\n    description: (\n      <>\n        Run your apps in Docker, host your code with Git, write documentation\n        with Swagger, and test your APIs while developing.\n      </>\n    ),\n  },\n];\n\nfunction Feature({ Svg, title, description }) {\n  return (\n    <div className={clsx(\"col col--4\")}>\n      <div className=\"text--center\">\n        <Svg className={styles.featureSvg} role=\"img\" />\n      </div>\n      <div className=\"text--center padding-horiz--md\">\n        <h3>{title}</h3>\n        <p>{description}</p>\n      </div>\n    </div>\n  );\n}\n\nexport default function HomepageFeatures() {\n  return (\n    <section className={styles.features}>\n      <div className=\"container\">\n        <div className=\"row\">\n          {FeatureList.map((props, idx) => (\n            <Feature key={idx} {...props} />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/HomepageFeatures/styles.module.css",
    "content": ".features {\n  display: flex;\n  align-items: center;\n  padding: 2rem 0;\n  width: 100%;\n}\n\n.featureSvg {\n  height: 200px;\n  width: 200px;\n}\n"
  },
  {
    "path": "docs/src/components/LockedVideoEmbed/index.js",
    "content": "import React from \"react\";\nimport Background from \"./background.png\";\n\nexport default function LockedVideoEmbed() {\n  return (\n    <div style={{ maxWidth: \"720px\", margin: \"4rem auto 4rem auto\" }}>\n      <div style={{ position: \"relative\", paddingTop: \"56.25%\" }}>\n        <div\n          style={{\n            border: \"none\",\n            position: \"absolute\",\n            top: \"0\",\n            left: \"0\",\n            height: \"100%\",\n            width: \"100%\",\n            textAlign: \"center\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <img\n            src={Background}\n            style={{\n              zIndex: 1,\n              position: \"absolute\",\n              top: \"0\",\n              left: \"0\",\n              height: \"100%\",\n              width: \"100%\",\n              objectFit: \"cover\",\n              filter: \"blur(5px) grayscale(80%) brightness(0.4)\",\n            }}\n          />\n          <svg\n            style={{ zIndex: 2 }}\n            viewBox=\"0 0 100 100\"\n            height=\"50%\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              d=\"M10.000 50.000 A40.000 40.000 0 1 0 90.000 50.000 A40.000 40.000 0 1 0 10.000 50.000 Z\"\n              fill=\"#D9EDFF\"\n            />\n            <path\n              d=\"M71.767,43.426a1.323,1.323,0,0,1,0,1.476C69.6,48.127,60.072,60.827,44.47,60.827s-25.129-12.7-27.3-15.925a1.326,1.326,0,0,1,0-1.476C19.341,40.2,28.868,27.5,44.47,27.5S69.6,40.2,71.767,43.426Z\"\n              fill=\"#ffffff\"\n            />\n            <path\n              d=\"M32.568 44.164 A11.902 11.902 0 1 0 56.372 44.164 A11.902 11.902 0 1 0 32.568 44.164 Z\"\n              fill=\"#B0D9FF\"\n            />\n            <path\n              d=\"M71.767,43.426a1.323,1.323,0,0,1,0,1.476C69.6,48.127,60.072,60.827,44.47,60.827s-25.129-12.7-27.3-15.925a1.326,1.326,0,0,1,0-1.476C19.341,40.2,28.868,27.5,44.47,27.5S69.6,40.2,71.767,43.426Z\"\n              fill=\"none\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M32.568 44.164 A11.902 11.902 0 1 0 56.372 44.164 A11.902 11.902 0 1 0 32.568 44.164 Z\"\n              fill=\"none\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M39.841 44.164 A4.629 4.629 0 1 0 49.099 44.164 A4.629 4.629 0 1 0 39.841 44.164 Z\"\n              fill=\"#ffffff\"\n            />\n            <path\n              d=\"M39.841 44.164 A4.629 4.629 0 1 0 49.099 44.164 A4.629 4.629 0 1 0 39.841 44.164 Z\"\n              fill=\"none\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M31.820 69.282 A12.65 1.725 0 1 0 57.120 69.282 A12.65 1.725 0 1 0 31.820 69.282 Z\"\n              fill=\"#B0D9FF\"\n            />\n            <path\n              d=\"M56.917 54.275 L80.377 54.275 L80.377 72.623 L56.917 72.623 Z\"\n              fill=\"#B0D9FF\"\n            />\n            <path\n              d=\"M59.263,54.275v-4.3A9.774,9.774,0,0,1,69.038,40.2h0a9.774,9.774,0,0,1,9.774,9.775v4.3H74.9v-4.3a5.865,5.865,0,0,0-5.865-5.865h0a5.865,5.865,0,0,0-5.865,5.865v4.3Z\"\n              fill=\"#ffffff\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M56.917 54.275 L80.377 54.275 L80.377 72.623 L56.917 72.623 Z\"\n              fill=\"none\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n            <path\n              d=\"M71.775,62.066a2.737,2.737,0,1,0-3.91,2.463v1.447a1.173,1.173,0,0,0,1.173,1.173h0a1.173,1.173,0,0,0,1.173-1.173V64.529A2.729,2.729,0,0,0,71.775,62.066Z\"\n              fill=\"#ffffff\"\n              stroke=\"#020064\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n            />\n          </svg>\n          <p\n            style={{\n              zIndex: 2,\n              textAlign: \"center\",\n              color: \"#020064\",\n              color: \"white\",\n              fontWeight: \"bolder\",\n            }}\n          >\n            This video is locked. Please{\" \"}\n            <a href=\"https://go.tecla.do/rest-apis-ebook\">\n              purchase the course\n            </a>{\" \"}\n            to view it.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/VideoEmbed/index.js",
    "content": "import React from \"react\";\n\nexport default function VideoEmbed({ url }) {\n  return (\n    <div style={{ position: \"relative\", paddingTop: \"56.25%\" }}>\n      <iframe\n        src={url}\n        style={{\n          border: \"none\",\n          position: \"absolute\",\n          top: \"0\",\n          left: \"0\",\n          height: \"100%\",\n          width: \"100%\",\n        }}\n        allow=\"accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;\"\n        allowFullScreen={true}\n      ></iframe>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "/**\n * Any CSS included here will be global. The classic template\n * bundles Infima by default. Infima is a CSS framework designed to\n * work well for content-centric websites.\n */\n\n/* You can override the default Infima variables here. */\n:root {\n  --ifm-color-primary: #2e8555;\n  --ifm-color-primary-dark: #29784c;\n  --ifm-color-primary-darker: #277148;\n  --ifm-color-primary-darkest: #205d3b;\n  --ifm-color-primary-light: #33925d;\n  --ifm-color-primary-lighter: #359962;\n  --ifm-color-primary-lightest: #3cad6e;\n  --ifm-code-font-size: 90%;\n  --ifm-code-padding-horizontal: 0.3rem;\n  --ifm-code-padding-vertical: 0.15rem;\n  --ifm-code-border-radius: 5px;\n  --aa-primary-color-rgb: 0, 0, 0 !important;\n  --aa-input-border-color-rgb: 0, 0, 0 !important;\n}\n\n.aa-DetachedSearchButton {\n  padding-right: 16px !important;\n  border-radius: 8px !important;\n}\n\n/* For readability concerns, you should choose a lighter palette in dark mode. */\n[data-theme='dark'] {\n  --ifm-color-primary: #25c2a0;\n  --ifm-color-primary-dark: #21af90;\n  --ifm-color-primary-darker: #1fa588;\n  --ifm-color-primary-darkest: #1a8870;\n  --ifm-color-primary-light: #29d5b0;\n  --ifm-color-primary-lighter: #32d8b4;\n  --ifm-color-primary-lightest: #4fddbf;\n}\n\n.docusaurus-highlight-code-line {\n  background-color: rgba(0, 0, 0, 0.1);\n  display: block;\n  margin: 0 calc(-1 * var(--ifm-pre-padding));\n  padding: 0 var(--ifm-pre-padding);\n}\n\n[data-theme='dark'] .docusaurus-highlight-code-line {\n  background-color: rgba(0, 0, 0, 0.3);\n}\n\n.codeTabContainer {\n  padding: 1rem;\n  background-color: rgba(46, 133, 85, 0.15);\n  border-radius: 8px;\n}\n\n.menu {\n  font-weight: normal;\n}\n\n.menu__caret::before, .menu__link--sublist-caret::after {\n  background: url(\"/img/folder-closed.svg\") no-repeat;\n  transform: none;\n}\n\n.menu__list-item--collapsed .menu__link--sublist::after, .menu__list-item--collapsed .menu__caret::before {\n  transform: none;\n}\n\n.menu__list-item-collapsible .menu__link--sublist[aria-expanded='true']::after,\n.menu__list-item-collapsible .menu__link--sublist[aria-expanded='true'] + button::before {\n  background: url(\"/img/folder-open.svg\") no-repeat;\n}\n\n.menu__list-item-collapsible .menu__link--sublist[aria-expanded='false']:hover::after,\n.menu__list-item-collapsible .menu__link--sublist[aria-expanded='false'] + button:hover::before {\n  fill: var(--ifm-color-primary-lightest);\n}\n\n.menu__link {\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n}\n\n.menu__list-item-collapsible, .theme-doc-sidebar-item-link-level-1 {\n  font-weight: var(--ifm-font-weight-bold);\n}\n\n.theme-doc-sidebar-item-category-level-1 .menu__list-item .menu__link {\n  border-left: 1px solid var(--ifm-color-gray-200);\n}\n\n.theme-doc-sidebar-item-category-level-1 .menu__list-item .menu__link:hover {\n  border-left: 1px solid var(--ifm-color-gray-600);\n  color: var(--ifm-color-gray-900);\n}\n\n.theme-doc-sidebar-item-category-level-1 .menu__list-item .menu__link--active {\n  border-left: 1px solid var(--ifm-color-primary-lightest);\n  font-weight: var(--ifm-font-weight-semibold);\n}\n\n.theme-doc-sidebar-item-category-level-1 .menu__list-item .menu__link--active:hover {\n  color: var(--ifm-color-primary);\n}\n\n.menu__link--active:not(.menu__link--sublist), .menu__list-item-collapsible--active {\n  background: unset;\n}\n\n.menu__link:hover, .menu__caret:hover {\n  background: inherit;\n}\n\n.menu__list-item:not(:first-child) {\n  margin-top: 0;\n}\n\n.menu__link[href]:hover, .menu__caret:hover {\n  color: var(--ifm-color-primary);\n}\n\n.menu__list-item-collapsible:hover {\n  background: inherit;\n}"
  },
  {
    "path": "docs/src/pages/index.js",
    "content": "import React from \"react\";\nimport clsx from \"clsx\";\nimport Layout from \"@theme/Layout\";\nimport Link from \"@docusaurus/Link\";\nimport useDocusaurusContext from \"@docusaurus/useDocusaurusContext\";\nimport styles from \"./index.module.css\";\nimport HomepageFeatures from \"@site/src/components/HomepageFeatures\";\nimport VideoEmbed from \"@site/src/components/VideoEmbed\";\n\nfunction HomepageHeader() {\n  const { siteConfig } = useDocusaurusContext();\n  return (\n    <header className={clsx(\"hero hero--primary\", styles.heroBanner)}>\n      <div className=\"container\">\n        <h1 className=\"hero__title\">{siteConfig.title}</h1>\n        <p className=\"hero__subtitle\">{siteConfig.tagline}</p>\n        <div className={styles.buttons}>\n          <Link\n            className=\"button button--secondary button--lg\"\n            to=\"/docs/course_intro/\"\n          >\n            Read the e-book\n          </Link>\n        </div>\n      </div>\n    </header>\n  );\n}\n\nexport default function Home() {\n  const { siteConfig } = useDocusaurusContext();\n  return (\n    <Layout\n      title={`${siteConfig.title} e-book`}\n      description=\"The full course notes and code\"\n    >\n      <HomepageHeader />\n      <main>\n        <div\n          style={{\n            border: \"4px solid black\",\n            maxWidth: \"800px\",\n            margin: \"4rem auto 4rem auto\",\n            boxShadow: \"0 5px 15px 0 rgba(0, 0, 0, 0.15)\",\n          }}\n        >\n          <VideoEmbed url=\"https://customer-zmitazl0ztnd2pvm.cloudflarestream.com/1c4db6119cf0c6e682a88a737af146eb/iframe?poster=https%3A%2F%2Fcustomer-zmitazl0ztnd2pvm.cloudflarestream.com%2F1c4db6119cf0c6e682a88a737af146eb%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600\" />\n        </div>\n        <HomepageFeatures />\n      </main>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "docs/src/pages/index.module.css",
    "content": "/**\n * CSS files with the .module.css suffix will be treated as CSS modules\n * and scoped locally.\n */\n\n.heroBanner {\n  padding: 4rem 0;\n  text-align: center;\n  position: relative;\n  overflow: hidden;\n}\n\n@media screen and (max-width: 996px) {\n  .heroBanner {\n    padding: 2rem;\n  }\n}\n\n.buttons {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "docs/src/pages/insomnia-files.md",
    "content": "# Insomnia Files for this course\n\nIn this course, we use [Insomnia](https://insomnia.rest/) to test our REST API as we develop it. It's a great tool, and relatively straightforward to use!\n\nTo make it easier for you, we've prepared a few files that you can import directly into your Insomnia app. Each section has its own file, and there's also a file which contains [all sections](#all-section-file-project).\n\n:::tip Get the files\nYou can download the entire GitHub repository as a .zip file ([see how here](https://github.com/tecladocode/rest-apis-flask-python#getting-started)). Then, extract it and you'll be able to find the JSON files for Insomnia.\n:::\n\n## Per-section files (collections)\n\nEvery section where we change the API endpoints, we've prepared an Insomnia file you can import. To import the file, just go to your Insomnia project, click on the \"Create\" button, and select \"Import from file\".\n\n![Screenshot showing dropdown appears when clicking 'create' button, select import from file there](./assets/in-import-file.png)\n\nThen, find the appropriate file and you should see the request collection being created. Files are in each section folder where API changes were introduced, and they are named `Insomnia_sectionX.json`.\n\n![Screenshot showing instructor finding the JSON file to import](./assets/in-import-file-select.png)\n\nThe Insomnia project should now show your new collection:\n\n![Screenshot showing the import succeeded and the collection appears](./assets/in-import-success.png)\n\nClicking on it should let you access the requests, which may be inside folders.\n\n![Screenshot showing the collection open, with the Stores folder expanded and the 'Create store' endpoint selected](./assets/in-section-import-s03.png)\n\n## All-section file (project)\n\nTo import all sections at the same time, you can use the all-section file. It's in the `docs/docs/` folder of the repository, and it's called [`Insomnia_all_sections.json`](https://github.com/tecladocode/rest-apis-flask-python/blob/develop/docs/docs/Insomnia_all_sections.json).\n\nTo import it, follow the same process as before but using this file. It will create multiple request collections.\n\nFirst, find the `Insomnia_all_sections.json` file when importing:\n\n![Screenshot showing the instructor finding the all-sections file when importing using Insomnia](./assets/in-import-all-sections-file-select.png)\n\nThis should import the request collections for all sections:\n\n![Screenshot showing the import succeeding, and request collections appear in Insomnia for every section where API changes were introduced](./assets/in-import-all-sections-success.png)\n\nThat's it! With this, you can either import sections individually, or all sections together to follow the entire course.\n\nNow let's start with the [course content](/docs/course_intro/)!"
  },
  {
    "path": "project/01-first-rest-api/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [\n    {\n        \"name\": \"My Store\",\n        \"items\": [\n            {\n                \"name\": \"Chair\",\n                \"price\": 15.99\n            }\n        ]\n    }\n]\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "project/02-first-rest-api-docker/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nRUN pip install flask\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "project/02-first-rest-api-docker/app.py",
    "content": "from flask import Flask, request\n\napp = Flask(__name__)\n\nstores = [\n    {\n        \"name\": \"My Store\",\n        \"items\": [\n            {\n                \"name\": \"Chair\",\n                \"price\": 15.99\n            }\n        ]\n    }\n]\n\n@app.get(\"/store\")\ndef get_stores():\n    return {\"stores\": stores}\n\n\n@app.post(\"/store\")\ndef create_store():\n    request_data = request.get_json()\n    new_store = {\"name\": request_data[\"name\"], \"items\": []}\n    stores.append(new_store)\n    return new_store, 201\n\n\n@app.post(\"/store/<string:name>/item\")\ndef create_item(name):\n    request_data = request.get_json()\n    for store in stores:\n        if store[\"name\"] == name:\n            new_item = {\"name\": request_data[\"name\"], \"price\": request_data[\"price\"]}\n            store[\"items\"].append(new_item)\n            return new_item, 201\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>\")\ndef get_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return store\n    return {\"message\": \"Store not found\"}, 404\n\n\n@app.get(\"/store/<string:name>/item\")\ndef get_item_in_store(name):\n    for store in stores:\n        if store[\"name\"] == name:\n            return {\"items\": store[\"items\"]}\n    return {\"message\": \"Store not found\"}, 404\n"
  },
  {
    "path": "project/03-items-stores-smorest/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/03-items-stores-smorest/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "project/03-items-stores-smorest/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\napp = Flask(__name__)\n\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n\napi = Api(app)\n\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\n"
  },
  {
    "path": "project/03-items-stores-smorest/db.py",
    "content": "\"\"\"\ndb.py\n---\n\nLater on, this file will be replaced by SQLAlchemy. For now, it mimics a database.\nOur data storage is:\n    - stores have a unique ID and a name\n    - items have a unique ID, a name, a price, and a store ID.\n\"\"\"\n\nstores = {}\nitems = {}\n"
  },
  {
    "path": "project/03-items-stores-smorest/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow"
  },
  {
    "path": "project/03-items-stores-smorest/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "project/03-items-stores-smorest/resources/item.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\n\nfrom schemas import ItemSchema, ItemUpdateSchema\nfrom db import items\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        try:\n            return items[item_id]\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    def delete(self, item_id):\n        try:\n            del items[item_id]\n            return {\"message\": \"Item deleted.\"}\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        try:\n            item = items[item_id]\n\n            # https://blog.teclado.com/python-dictionary-merge-update-operators/\n            item |= item_data\n\n            return item\n        except KeyError:\n            abort(404, message=\"Item not found.\")\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return items.values()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        for item in items.values():\n            if (\n                item_data[\"name\"] == item[\"name\"]\n                and item_data[\"store_id\"] == item[\"store_id\"]\n            ):\n                abort(400, message=f\"Item already exists.\")\n\n        item_id = uuid.uuid4().hex\n        item = {**item_data, \"id\": item_id}\n        items[item_id] = item\n\n        return item\n"
  },
  {
    "path": "project/03-items-stores-smorest/resources/store.py",
    "content": "import uuid\nfrom flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom db import stores\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(cls, store_id):\n        try:\n            # You presumably would want to include the store's items here too\n            # More on that when we look at databases\n            return stores[store_id]\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n    def delete(cls, store_id):\n        try:\n            del stores[store_id]\n            return {\"message\": \"Store deleted.\"}\n        except KeyError:\n            abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(cls):\n        return stores.values()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(cls, store_data):\n        for store in stores.values():\n            if store_data[\"name\"] == store[\"name\"]:\n                abort(400, message=f\"Store already exists.\")\n\n        store_id = uuid.uuid4().hex\n        store = {**store_data, \"id\": store_id}\n        stores[store_id] = store\n\n        return store\n"
  },
  {
    "path": "project/03-items-stores-smorest/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n    store_id = fields.Int(required=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(Schema):\n    id = fields.Str(dump_only=True)\n    name = fields.Str(required=True)\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nfrom db import db\n\nimport models\n\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n\n    return app\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.store import StoreModel\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/requirements.txt",
    "content": "flask\nflask-smorest\npython-dotenv\nmarshmallow\nsqlalchemy\nflask-sqlalchemy"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/resources/__init__.py",
    "content": ""
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "project/04-items-stores-smorest-sqlalchemy/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n"
  },
  {
    "path": "project/05-add-many-to-many/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/05-add-many-to-many/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "project/05-add-many-to-many/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    api = Api(app)\n\n    with app.app_context():\n        db.create_all()\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "project/05-add-many-to-many/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "project/05-add-many-to-many/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/05-add-many-to-many/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "project/05-add-many-to-many/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "project/05-add-many-to-many/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "project/05-add-many-to-many/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "project/05-add-many-to-many/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "project/05-add-many-to-many/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "project/05-add-many-to-many/resources/__init__.py",
    "content": ""
  },
  {
    "path": "project/05-add-many-to-many/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "project/05-add-many-to-many/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "project/05-add-many-to-many/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "project/06-add-db-migrations/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/06-add-db-migrations/.python-version",
    "content": "3.10.4\n"
  },
  {
    "path": "project/06-add-db-migrations/Dockerfile",
    "content": "FROM python:3.10\nEXPOSE 5000\nWORKDIR /app\nCOPY ./requirements.txt requirements.txt\nRUN pip install --no-cache-dir --upgrade -r requirements.txt\nCOPY . .\nCMD [\"flask\", \"run\", \"--host\", \"0.0.0.0\"]"
  },
  {
    "path": "project/06-add-db-migrations/app.py",
    "content": "from flask import Flask\nfrom flask_smorest import Api\nfrom flask_migrate import Migrate\n\nimport models\n\nfrom db import db\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\ndef create_app(db_url=None):\n    app = Flask(__name__)\n    app.config[\"API_TITLE\"] = \"Stores REST API\"\n    app.config[\"API_VERSION\"] = \"v1\"\n    app.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\n    app.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\n    app.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\n    app.config[\n        \"OPENAPI_SWAGGER_UI_URL\"\n    ] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\n    app.config[\"SQLALCHEMY_DATABASE_URI\"] = db_url or \"sqlite:///data.db\"\n    app.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\n    app.config[\"PROPAGATE_EXCEPTIONS\"] = True\n    db.init_app(app)\n    migrate = Migrate(app, db)\n    api = Api(app)\n\n    api.register_blueprint(ItemBlueprint)\n    api.register_blueprint(StoreBlueprint)\n    api.register_blueprint(TagBlueprint)\n\n    return app\n"
  },
  {
    "path": "project/06-add-db-migrations/conftest.py",
    "content": "import pytest\nfrom app import create_app\n\n\n@pytest.fixture()\ndef app():\n    app = create_app(\"sqlite://\")\n    app.config.update(\n        {\n            \"TESTING\": True,\n        }\n    )\n\n    yield app\n\n\n@pytest.fixture()\ndef client(app):\n    return app.test_client()\n"
  },
  {
    "path": "project/06-add-db-migrations/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/README",
    "content": "Single-database configuration for Flask.\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic,flask_migrate\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[logger_flask_migrate]\nlevel = INFO\nhandlers =\nqualname = flask_migrate\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/env.py",
    "content": "from __future__ import with_statement\n\nimport logging\nfrom logging.config import fileConfig\n\nfrom flask import current_app\n\nfrom alembic import context\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\nlogger = logging.getLogger('alembic.env')\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nconfig.set_main_option(\n    'sqlalchemy.url',\n    str(current_app.extensions['migrate'].db.get_engine().url).replace(\n        '%', '%%'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(\n        url=url, target_metadata=target_metadata, literal_binds=True\n    )\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n\n    # this callback is used to prevent an auto-migration from being generated\n    # when there are no changes to the schema\n    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html\n    def process_revision_directives(context, revision, directives):\n        if getattr(config.cmd_opts, 'autogenerate', False):\n            script = directives[0]\n            if script.upgrade_ops.is_empty():\n                directives[:] = []\n                logger.info('No changes in schema detected.')\n\n    connectable = current_app.extensions['migrate'].db.get_engine()\n\n    with connectable.connect() as connection:\n        context.configure(\n            connection=connection,\n            target_metadata=target_metadata,\n            process_revision_directives=process_revision_directives,\n            **current_app.extensions['migrate'].configure_args\n        )\n\n        with context.begin_transaction():\n            context.run_migrations()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\nbranch_labels = ${repr(branch_labels)}\ndepends_on = ${repr(depends_on)}\n\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/versions/5acd69659946_.py",
    "content": "\"\"\"empty message\n\nRevision ID: 5acd69659946\nRevises: \nCreate Date: 2022-06-17 14:13:44.923682\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = '5acd69659946'\ndown_revision = None\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('stores',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('price', sa.Float(precision=2), nullable=False),\n    sa.Column('store_id', sa.Integer(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=80), nullable=False),\n    sa.Column('store_id', sa.String(), nullable=False),\n    sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('items_tags',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('item_id', sa.Integer(), nullable=True),\n    sa.Column('tag_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['item_id'], ['items.id'], ),\n    sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('items_tags')\n    op.drop_table('tags')\n    op.drop_table('items')\n    op.drop_table('stores')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "project/06-add-db-migrations/migrations/versions/a40bdfbd7a9d_.py",
    "content": "\"\"\"empty message\n\nRevision ID: a40bdfbd7a9d\nRevises: 5acd69659946\nCreate Date: 2022-06-17 14:19:34.934726\n\n\"\"\"\nfrom alembic import op\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = 'a40bdfbd7a9d'\ndown_revision = '5acd69659946'\nbranch_labels = None\ndepends_on = None\n\n\ndef upgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('items', sa.Column('description', sa.String(), nullable=True))\n    # ### end Alembic commands ###\n\n\ndef downgrade():\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('items', 'description')\n    # ### end Alembic commands ###\n"
  },
  {
    "path": "project/06-add-db-migrations/models/__init__.py",
    "content": "from models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "project/06-add-db-migrations/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    description = db.Column(db.String)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n"
  },
  {
    "path": "project/06-add-db-migrations/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "project/06-add-db-migrations/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    tags = db.relationship(\"TagModel\", back_populates=\"store\", lazy=\"dynamic\")\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n"
  },
  {
    "path": "project/06-add-db-migrations/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    store_id = db.Column(db.Integer, db.ForeignKey(\"stores.id\"), nullable=False)\n\n    store = db.relationship(\"StoreModel\", back_populates=\"tags\")\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n"
  },
  {
    "path": "project/06-add-db-migrations/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\nFlask-Migrate\npasslib\nmarshmallow\npython-dotenv\ngunicorn"
  },
  {
    "path": "project/06-add-db-migrations/resources/__init__.py",
    "content": ""
  },
  {
    "path": "project/06-add-db-migrations/resources/__tests__/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture()\ndef created_store_id(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_item_id(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    return response.json[\"id\"]\n\n\n@pytest.fixture()\ndef created_tag_id(client, created_store_id):\n    response = client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    return response.json[\"id\"]\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/__tests__/test_item.py",
    "content": "def test_create_item_in_store(client, created_store_id):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_create_item_with_store_id_not_found(client):\n    # Note that this will fail if foreign key constraints are enabled.\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] is None\n\n\ndef test_create_item_with_unknown_data(client):\n    response = client.post(\n        \"/item\",\n        json={\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n            \"store_id\": 1,\n            \"unknown_field\": \"unknown\",\n        },\n    )\n\n    assert response.status_code == 422\n    assert response.json[\"errors\"][\"json\"][\"unknown_field\"] == [\"Unknown field.\"]\n\n\ndef test_delete_item(client, created_item_id):\n    response = client.delete(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"message\"] == \"Item deleted.\"\n\n\ndef test_update_item(client, created_item_id):\n    response = client.put(\n        f\"/item/{created_item_id}\",\n        json={\"name\": \"Test Item (updated)\", \"price\": 12.5},\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item (updated)\"\n    assert response.json[\"price\"] == 12.5\n\n\ndef test_get_all_items(client):\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n    response = client.post(\n        \"/item\",\n        json={\"name\": \"Test Item 2\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 2\n    assert response.json[0][\"name\"] == \"Test Item\"\n    assert response.json[0][\"price\"] == 10.5\n    assert response.json[1][\"name\"] == \"Test Item 2\"\n\n\ndef test_get_all_items_empty(client):\n    response = client.get(\n        \"/item\",\n    )\n\n    assert response.status_code == 200\n    assert len(response.json) == 0\n\n\ndef test_get_item_details(client, created_item_id, created_store_id):\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"store\"] == {\"id\": created_store_id, \"name\": \"Test Store\"}\n\n\ndef test_get_item_details_with_tag(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n    response = client.get(\n        f\"/item/{created_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"name\"] == \"Test Item\"\n    assert response.json[\"price\"] == 10.5\n    assert response.json[\"tags\"] == [{\"id\": created_tag_id, \"name\": \"Test Tag\"}]\n\n\ndef test_get_item_detail_not_found(client):\n    response = client.get(\n        \"/item/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/__tests__/test_store.py",
    "content": "def test_get_store(client, created_store_id):\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Store\",\n        \"items\": [],\n        \"tags\": [],\n    }\n\n\ndef test_get_store_not_found(client):\n    response = client.get(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_with_item(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": created_store_id},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_get_store_with_tag(client, created_store_id):\n    client.post(\n        f\"/store/{created_store_id}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"tags\"] == [{\"id\": 1, \"name\": \"Test Tag\"}]\n\n\ndef test_create_store(client):\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    assert response.status_code == 201\n    assert response.json[\"name\"] == \"Test Store\"\n\n\ndef test_create_store_with_items(client, created_store_id):\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    # Get the store with id 1 and check the items contains the newly created item\n    response = client.get(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_delete_store(client, created_store_id):\n    response = client.delete(\n        f\"/store/{created_store_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\"message\": \"Store deleted\"}\n\n\ndef test_delete_store_doesnt_exist(client):\n    response = client.delete(\n        \"/store/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_store_list_empty(client):\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == []\n\n\ndef test_get_store_list_single(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [{\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []}]\n\n\ndef test_get_store_list_multiple(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store 2\"},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\"id\": 1, \"name\": \"Test Store\", \"items\": [], \"tags\": []},\n        {\"id\": 2, \"name\": \"Test Store 2\", \"items\": [], \"tags\": []},\n    ]\n\n\ndef test_get_store_list_with_items(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        \"/item\",\n        json={\"name\": \"Test Item\", \"price\": 10.5, \"store_id\": 1},\n    )\n\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [\n                {\n                    \"id\": 1,\n                    \"name\": \"Test Item\",\n                    \"price\": 10.5,\n                }\n            ],\n            \"tags\": [],\n        }\n    ]\n\n\ndef test_get_store_list_with_tags(client):\n    resp = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    client.post(\n        f\"/store/{resp.json['id']}/tag\",\n        json={\"name\": \"Test Tag\"},\n    )\n    response = client.get(\n        \"/store\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Store\",\n            \"items\": [],\n            \"tags\": [{\"id\": 1, \"name\": \"Test Tag\"}],\n        }\n    ]\n\n\ndef test_create_store_duplicate_name(client):\n    client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n\n    response = client.post(\n        \"/store\",\n        json={\"name\": \"Test Store\"},\n    )\n    assert response.status_code == 400\n    assert response.json[\"message\"] == \"A store with that name already exists.\"\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/__tests__/test_tag.py",
    "content": "import pytest\nimport logging\n\nLOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture()\ndef created_tag_with_item_id(client, created_item_id, created_tag_id):\n    client.post(f\"/item/{created_item_id}/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    return response.json[\"id\"]\n\n\ndef test_get_tag(client, created_tag_id):\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == {\n        \"id\": 1,\n        \"name\": \"Test Tag\",\n        \"items\": [],\n        \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n    }\n\n\ndef test_get_tag_not_found(client):\n    response = client.get(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_items_linked_with_tag(client, created_tag_with_item_id):\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == [\n        {\n            \"id\": 1,\n            \"name\": \"Test Item\",\n            \"price\": 10.5,\n        }\n    ]\n\n\ndef test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):\n    client.delete(f\"/item/{created_item_id}/tag/{created_tag_with_item_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_with_item_id}\",\n    )\n\n    assert response.status_code == 200\n    assert response.json[\"items\"] == []\n\n\ndef test_delete_tag_without_items(client, created_tag_id):\n    delete_response = client.delete(f\"/tag/{created_tag_id}\")\n\n    response = client.get(\n        f\"/tag/{created_tag_id}\",\n    )\n\n    assert delete_response.status_code == 202\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_delete_tag_still_has_items(client, created_tag_with_item_id):\n    response = client.delete(f\"/tag/{created_tag_with_item_id}\")\n\n    assert response.status_code == 400\n    assert (\n        response.json[\"message\"]\n        == \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n    )\n\n\ndef test_delete_tag_not_found(client):\n    response = client.delete(\n        \"/tag/1\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n\n\ndef test_get_all_tags_in_store(client, created_store_id, created_tag_id):\n    response = client.get(\n        f\"/store/{created_store_id}/tag\",\n    )\n\n    assert response.status_code == 200\n    assert response.json == [\n        {\n            \"id\": created_tag_id,\n            \"name\": \"Test Tag\",\n            \"items\": [],\n            \"store\": {\"id\": 1, \"name\": \"Test Store\"},\n        }\n    ]\n\n\ndef test_get_all_tags_in_store_not_found(client):\n    response = client.get(\n        \"/store/1/tag\",\n    )\n\n    assert response.status_code == 404\n    assert response.json == {\"code\": 404, \"status\": \"Not Found\"}\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:item_id>\")\nclass Item(MethodView):\n    @blp.response(200, ItemSchema)\n    def get(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        return item\n\n    def delete(self, item_id):\n        item = ItemModel.query.get_or_404(item_id)\n        db.session.delete(item)\n        db.session.commit()\n        return {\"message\": \"Item deleted.\"}\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, item_id):\n        item = ItemModel.query.get(item_id)\n\n        if item:\n            item.price = item_data[\"price\"]\n            item.name = item_data[\"name\"]\n        else:\n            item = ItemModel(id=item_id, **item_data)\n\n        db.session.add(item)\n        db.session.commit()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.query.all()\n\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data):\n        item = ItemModel(**item_data)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError, IntegrityError\n\nfrom db import db\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:store_id>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        return store\n\n    def delete(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n        db.session.delete(store)\n        db.session.commit()\n        return {\"message\": \"Store deleted\"}, 200\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(self):\n        return StoreModel.query.all()\n\n    @blp.arguments(StoreSchema)\n    @blp.response(201, StoreSchema)\n    def post(self, store_data):\n        store = StoreModel(**store_data)\n        try:\n            db.session.add(store)\n            db.session.commit()\n        except IntegrityError:\n            abort(\n                400,\n                message=\"A store with that name already exists.\",\n            )\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n"
  },
  {
    "path": "project/06-add-db-migrations/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom db import db\nfrom models import TagModel, StoreModel, ItemModel\nfrom schemas import TagSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/store/<string:store_id>/tag\")\nclass TagsInStore(MethodView):\n    @blp.response(200, TagSchema(many=True))\n    def get(self, store_id):\n        store = StoreModel.query.get_or_404(store_id)\n\n        return store.tags.all()  # lazy=\"dynamic\" means 'tags' is a query\n\n    @blp.arguments(TagSchema)\n    @blp.response(201, TagSchema)\n    def post(self, tag_data, store_id):\n        if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data[\"name\"]).first():\n            abort(400, message=\"A tag with that name already exists in that store.\")\n\n        tag = TagModel(**tag_data, store_id=store_id)\n\n        try:\n            db.session.add(tag)\n            db.session.commit()\n        except SQLAlchemyError as e:\n            abort(\n                500,\n                message=str(e),\n            )\n\n        return tag\n\n\n@blp.route(\"/item/<string:item_id>/tag/<string:tag_id>\")\nclass LinkTagsToItem(MethodView):\n    @blp.response(201, TagSchema)\n    def post(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.append(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.response(200, TagAndItemSchema)\n    def delete(self, item_id, tag_id):\n        item = ItemModel.query.get_or_404(item_id)\n        tag = TagModel.query.get_or_404(tag_id)\n\n        item.tags.remove(tag)\n\n        try:\n            db.session.add(item)\n            db.session.commit()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return {\"message\": \"Item removed from tag\", \"item\": item, \"tag\": tag}\n\n\n@blp.route(\"/tag/<string:tag_id>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n        return tag\n\n    @blp.response(\n        202,\n        description=\"Deletes a tag if no item is tagged with it.\",\n        example={\"message\": \"Tag deleted.\"},\n    )\n    @blp.alt_response(404, description=\"Tag not found.\")\n    @blp.alt_response(\n        400,\n        description=\"Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.\",\n    )\n    def delete(self, tag_id):\n        tag = TagModel.query.get_or_404(tag_id)\n\n        if not tag.items:\n            db.session.delete(tag)\n            db.session.commit()\n            return {\"message\": \"Tag deleted.\"}\n        abort(\n            400,\n            message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n"
  },
  {
    "path": "project/06-add-db-migrations/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass PlainItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True)\n    price = fields.Float(required=True)\n\n\nclass PlainStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass PlainTagSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str()\n\n\nclass ItemSchema(PlainItemSchema):\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    name = fields.Str()\n    price = fields.Float()\n\n\nclass StoreSchema(PlainStoreSchema):\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)\n\n\nclass TagSchema(PlainTagSchema):\n    store_id = fields.Int(load_only=True)\n    items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)\n    store = fields.Nested(PlainStoreSchema(), dump_only=True)\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n"
  },
  {
    "path": "project/using-flask-restful/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/using-flask-restful/Flask-JWT-Extended.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"74a1833f-bc4e-4e85-a525-72d268ab9999\",\n\t\t\"name\": \"Flask-JWT-Extended\",\n\t\t\"description\": \"This collection contains requests associated witht the Flask-JWT-Extended section of the REST API course.\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"users\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"register a new user\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/register\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"register\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/1\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"1\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/2\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"2\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"login\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"8c0c0ed6-c206-4c88-9349-429e024e312b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"refresh token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.refresh_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\",\n\t\t\t\t\t\t\t\t\t\"// set refresh token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.refresh_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"jose\\\",\\n  \\\"password\\\" : \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/login\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"login\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"logout\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"dc763e9b-e6c7-4ff3-9766-637976a5c64b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/logout\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"logout\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"refresh token\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"ad818ea6-8f79-436e-b756-ad878666ae9e\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{refresh_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/refresh\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"refresh\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"items\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"post item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"put item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete item by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items without JWT\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"stores\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"create a new store\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete a new store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all stores\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"stores\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "project/using-flask-restful/Stores_REST_API_2022-01-14.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-01-14T11:50:51.742Z\",\"__export_source\":\"insomnia.desktop.app:v2021.7.2\",\"resources\":[{\"_id\":\"req_efcadee1c4fc48f099644e23398a5d29\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159057139,\"created\":1642157007062,\"url\":\"{{url}}/register\",\"name\":\"/register\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_64c3752c7f694f0aa830bacba3b35aea\"},{\"name\":\"Authorization\",\"value\":\"JWT\",\"id\":\"pair_d143b36c4aa74f9681dc1590970da3b7\"}],\"authentication\":{},\"metaSortKey\":-1642157660252,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157670592,\"created\":1642157670592,\"name\":\"Authentication\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157670592,\"_type\":\"request_group\"},{\"_id\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"parentId\":null,\"modified\":1642157007080,\"created\":1642149963161,\"name\":\"Stores REST API\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_16415c75944342dab73119513e7bd20b\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159087108,\"created\":1642157007061,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_a8caec1064eb43b7ac5c8c9294be13a3\"}],\"authentication\":{},\"metaSortKey\":-1642157660202,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b08c51961bea4413a31fba1af93b3759\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642159093249,\"created\":1642157007054,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 3\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"},{\"id\":\"pair_4926e48dcb594eaa9c79a78b801b708f\",\"name\":\"Authorization\",\"value\":\"Bearer {% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always', 60 %}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157649712,\"created\":1642157649712,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157649712,\"_type\":\"request_group\"},{\"_id\":\"req_e553e5091f714becb81e1b27bfc8f34b\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642160992156,\"created\":1642157007053,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1f64b6c8fc8642aa9c267c8d49c72435\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157976127,\"created\":1642157007052,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_62498017fcb34ba0a3a19b4e0f2d4499\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_69d7ed86b4dc4b72a72778f97a77e05c\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157658823,\"created\":1642157007048,\"url\":\"{{url}}/item\",\"name\":\"/item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007178.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_054be716de114cc49d6e49d04a5a901b\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642159101503,\"created\":1642157684047,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"item_id\\\": {% response 'body', 'req_e553e5091f714becb81e1b27bfc8f34b', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_adac84f9834d4e948ceb02807787c935\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157684028,\"created\":1642157684028,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157641282.5,\"_type\":\"request_group\"},{\"_id\":\"req_a5b2631adf9e4ef894a8a1b9d2c77aa8\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158069229,\"created\":1642157684046,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"JWT \",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5dbef7a9b30d4ee68c148c4af447c241\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158073367,\"created\":1642157684041,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_7e8e3838e1cb41b485e091bf667b0764\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157645538,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320140.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157632853,\"created\":1642157632853,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157632853,\"_type\":\"request_group\"},{\"_id\":\"req_f99ce0192797434f99657221acc45fe3\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157643850,\"created\":1642157007059,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320090.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_82705a91a36849b09f1347d135816761\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157642682,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320040.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_94efb5c0488d43d8be95fd82b33afb97\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157639015,\"created\":1642157007060,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157319990.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9bc4db6f4d02466aba86edef29722854\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157781373,\"created\":1642157007070,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_8c8f8b7b9ddd4c3ca7fb6df5418b7f2e\"}],\"authentication\":{},\"metaSortKey\":-1642157007070,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157017423,\"created\":1642157007072,\"name\":\"User create store and item\",\"description\":\"Check user can register.\\nCheck user can create store.\\nCheck user can create item in store.\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157007128,\"_type\":\"request_group\"},{\"_id\":\"req_2ce4ecd840094ac1a164d7a0bfbb6d83\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157887543,\"created\":1642157007069,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007069,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8c68f13e77a74937b62cde1ae24bed61\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157923539,\"created\":1642157007068,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/test_item in test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": {% response 'body', 'req_2ce4ecd840094ac1a164d7a0bfbb6d83', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_d0c5451b8b044511b76436c627ffc4bb\"},{\"id\":\"pair_eb8ca7f686334ae5a48ad48412436ad9\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007068,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_297f6099ee274c1f8ccceb3bc29ad582\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007065,\"created\":1642157007065,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007065,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_84a807bef7cd4a66bc81f5401f0639cd\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157931016,\"created\":1642157007064,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/my_item copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_daca8133eb94474ca84748a0e4c8bcaf\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007064,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_10076b1332f2458e897d7b5200c7e5de\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007063,\"created\":1642157007063,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/<name> copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007063,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642160933906,\"created\":1642149963165,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5000\",\"access_token\":\"{% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'never', 60 %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1642149963165,\"_type\":\"environment\"},{\"_id\":\"jar_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963166,\"created\":1642149963166,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_f25b8aff2219447aa56189a385b1663c\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963162,\"created\":1642149963162,\"fileName\":\"Stores REST API\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "project/using-flask-restful/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_restful import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom resources.user import UserRegister, UserLogin, User, TokenRefresh, UserLogout\nfrom resources.item import Item, ItemList\nfrom resources.store import Store, StoreList\nfrom resources.tag import Tag\n\napp = Flask(__name__)\napp.config[\"SQLALCHEMY_DATABASE_URI\"] = \"sqlite:///data.db\"\napp.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\ndb.init_app(app)\napi = Api(app)\n\n\"\"\"\nJWT related configuration. The following functions includes:\n1) add claims to each jwt\n2) customize the token expired error message\n\"\"\"\napp.config[\"JWT_SECRET_KEY\"] = \"jose\"\njwt = JWTManager(app)\n\n\"\"\"\n`claims` are data we choose to attach to each jwt payload\nand for each jwt protected endpoint, we can retrieve these claims via `get_jwt_claims()`\none possible use case for claims are access level control, which is shown below\n\"\"\"\n\n\n@jwt.additional_claims_loader\ndef add_claims_to_jwt(identity):\n    # TODO: Read from a config file instead of hard-coding\n    if identity == 1:\n        return {\"is_admin\": True}\n    return {\"is_admin\": False}\n\n\n@jwt.token_in_blocklist_loader\ndef check_if_token_in_blocklist(jwt_header, jwt_payload):\n    return jwt_payload[\"jti\"] in BLOCKLIST\n\n\n@jwt.expired_token_loader\ndef expired_token_callback(jwt_header, jwt_payload):\n    return jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}), 401\n\n\n@jwt.invalid_token_loader\ndef invalid_token_callback(error):\n    return (\n        jsonify(\n            {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n        ),\n        401,\n    )\n\n\n@jwt.unauthorized_loader\ndef missing_token_callback(error):\n    return (\n        jsonify(\n            {\n                \"description\": \"Request does not contain an access token.\",\n                \"error\": \"authorization_required\",\n            }\n        ),\n        401,\n    )\n\n\n@jwt.needs_fresh_token_loader\ndef token_not_fresh_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token is not fresh.\", \"error\": \"fresh_token_required\"}\n        ),\n        401,\n    )\n\n\n@jwt.revoked_token_loader\ndef revoked_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n        ),\n        401,\n    )\n\n\n# JWT configuration ends\n\n\nwith app.app_context():\n    import models  # noqa: F401\n\n    db.create_all()\n\n\napi.add_resource(UserRegister, \"/register\")\napi.add_resource(UserLogin, \"/login\")\napi.add_resource(UserLogout, \"/logout\")\napi.add_resource(User, \"/user/<int:user_id>\")\napi.add_resource(TokenRefresh, \"/refresh\")\napi.add_resource(Store, \"/store/<string:name>\")\napi.add_resource(StoreList, \"/store\")\napi.add_resource(Item, \"/item/<string:name>\")\napi.add_resource(ItemList, \"/item\")\napi.add_resource(Tag, \"/tag/<string:name>\")\n"
  },
  {
    "path": "project/using-flask-restful/blocklist.py",
    "content": "\"\"\"\nblacklist.py\n\nThis file just contains the blacklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blacklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "project/using-flask-restful/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/using-flask-restful/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "project/using-flask-restful/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"price\": self.price,\n            \"store_id\": self.store_id,\n            \"tags\": [tag.json() for tag in self.tags],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restful/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "project/using-flask-restful/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.json() for item in self.items.all()],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restful/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.name for item in self.items],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restful/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = 'users'\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n\n    def json(self):\n        return {\n            'id': self.id,\n            'username': self.username\n        }\n\n    @classmethod\n    def find_by_username(cls, username):\n        return cls.query.filter_by(username=username).first()\n\n    @classmethod\n    def find_by_id(cls, _id):\n        return cls.query.filter_by(id=_id).first()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restful/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-RESTful\nFlask-SQLAlchemy\npasslib\npython-dotenv\n"
  },
  {
    "path": "project/using-flask-restful/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "project/using-flask-restful/resources/item.py",
    "content": "from flask_restful import Resource, reqparse\nfrom flask_jwt_extended import get_jwt_identity, jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import ItemModel\n\n\nclass Item(Resource):\n    parser = reqparse.RequestParser()\n    parser.add_argument(\n        \"price\", type=float, required=True, help=\"This field cannot be left blank!\"\n    )\n    parser.add_argument(\n        \"store_id\", type=int, required=True, help=\"Every item needs a store_id.\"\n    )\n\n    @jwt_required()\n    def get(self, name):\n        item = ItemModel.find_by_name(name)\n        if item:\n            return item.json()\n        return {\"message\": \"Item not found\"}, 404\n\n    @jwt_required(fresh=True)\n    def post(self, name):\n        if ItemModel.find_by_name(name):\n            return {\n                \"message\": \"An item with name '{}' already exists.\".format(name)\n            }, 400\n\n        data = self.parser.parse_args()\n\n        item = ItemModel(name=name, **data)\n\n        try:\n            item.save_to_db()\n        except SQLAlchemyError:\n            return {\"message\": \"An error occurred while inserting the item.\"}, 500\n\n        return item.json(), 201\n\n    @jwt_required()\n    def delete(self, name):\n        jwt = get_jwt()\n        if not jwt[\"is_admin\"]:\n            return {\"message\": \"Admin privilege required.\"}, 401\n\n        item = ItemModel.find_by_name(name)\n        if item:\n            item.delete_from_db()\n            return {\"message\": \"Item deleted.\"}\n        return {\"message\": \"Item not found.\"}, 404\n\n    def put(self, name):\n        data = self.parser.parse_args()\n\n        item = ItemModel.find_by_name(name)\n\n        if item:\n            item.price = data[\"price\"]\n        else:\n            item = ItemModel(name, **data)\n\n        item.save_to_db()\n\n        return item.json()\n\n\nclass ItemList(Resource):\n    @jwt_required(optional=True)\n    def get(self):\n        user_id = get_jwt_identity()\n        items = [item.json() for item in ItemModel.find_all()]\n        if user_id:\n            return {\"items\": items}, 200\n        return {\n            \"items\": [item[\"name\"] for item in items],\n            \"message\": \"More data available if you log in.\",\n        }, 200\n"
  },
  {
    "path": "project/using-flask-restful/resources/store.py",
    "content": "from flask_restful import Resource\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import StoreModel\n\n\nclass Store(Resource):\n    @classmethod\n    def get(cls, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            return store.json()\n        return {\"message\": \"Store not found\"}, 404\n\n    @classmethod\n    def post(cls, name):\n        if StoreModel.find_by_name(name):\n            return {\n                \"message\": \"A store with name '{}' already exists.\".format(name)\n            }, 400\n\n        store = StoreModel(name=name)\n        try:\n            store.save_to_db()\n        except SQLAlchemyError:\n            return {\"message\": \"An error occurred creating the store.\"}, 500\n\n        return store.json(), 201\n\n    @classmethod\n    def delete(cls, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            store.delete_from_db()\n            return {\"message\": \"Store deleted\"}, 200\n        return {\"message\": \"Store not found\"}, 404\n\n\nclass StoreList(Resource):\n    @classmethod\n    def get(cls):\n        return {\"stores\": [store.json() for store in StoreModel.find_all()]}\n"
  },
  {
    "path": "project/using-flask-restful/resources/tag.py",
    "content": "from flask_restful import Resource, reqparse\nfrom werkzeug.exceptions import BadRequest\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import TagModel\nfrom models import ItemModel\n\n\nclass Tag(Resource):\n    parser = reqparse.RequestParser()\n    parser.add_argument(\n        \"item_id\",\n        type=int,\n        required=True,\n        help=\"To create or add a tag to an item, please provide the item_id.\",\n    )\n\n    def get(self, name):\n        tag = TagModel.find_by_name(name)\n        if tag:\n            return tag.json()\n        return {\"message\": \"Tag not found\"}, 404\n\n    def post(self, name):\n        tag = TagModel.find_by_name(name)\n        if not tag:\n            tag = TagModel(name=name)\n\n        # Add the item to the tag\n        data = self.parser.parse_args()\n        item = ItemModel.query.get(data[\"item_id\"])\n\n        if not item:\n            return {\"message\": \"An item with this item_id doesn't exist.\"}, 400\n\n        tag.items.append(item)\n\n        try:\n            tag.save_to_db()\n        except SQLAlchemyError:\n            return {\"message\": \"An error occurred while inserting the tag.\"}, 500\n\n        return tag.json(), 201\n\n    def delete(self, name):\n        tag = TagModel.find_by_name(name)\n        try:\n            data = self.parser.parse_args()\n            if \"item_id\" in data:\n                item = ItemModel.query.get(data[\"item_id\"])\n                tag.items.remove(item)\n                return {\n                    \"message\": \"Item removed from tag\",\n                    \"item\": item.json(),\n                    \"tag\": tag.json(),\n                }\n        except BadRequest:\n            # Assume no item_id was passed. Instead delete entire tag.\n            # First check tag has no items\n            if not tag.items:\n                tag.delete_from_db()\n                return {\"message\": \"Tag deleted.\"}\n            return {\n                \"message\": \"Could not delete tag. Make sure tag is not associated with any items, then try again.\"\n            }\n        return {\"message\": \"Tag not found.\"}, 404\n"
  },
  {
    "path": "project/using-flask-restful/resources/user.py",
    "content": "from flask_restful import Resource, reqparse\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom models import UserModel\nfrom blocklist import BLOCKLIST\n\n_user_parser = reqparse.RequestParser()\n_user_parser.add_argument(\n    \"username\", type=str, required=True, help=\"This field cannot be blank.\"\n)\n_user_parser.add_argument(\n    \"password\", type=str, required=True, help=\"This field cannot be blank.\"\n)\n\n\nclass UserRegister(Resource):\n    def post(self):\n        data = _user_parser.parse_args()\n\n        if UserModel.find_by_username(data[\"username\"]):\n            return {\"message\": \"A user with that username already exists\"}, 400\n\n        user = UserModel(\n            username=data[\"username\"], password=pbkdf2_sha256.hash(data[\"password\"])\n        )\n        user.save_to_db()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\nclass UserLogin(Resource):\n    def post(self):\n        data = _user_parser.parse_args()\n\n        user = UserModel.find_by_username(data[\"username\"])\n\n        if user and pbkdf2_sha256.verify(data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        return {\"message\": \"Invalid Credentials!\"}, 401\n\n\nclass UserLogout(Resource):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\nclass User(Resource):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @classmethod\n    def get(cls, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            return {\"message\": \"User Not Found\"}, 404\n        return user.json(), 200\n\n    def delete(self, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            return {\"message\": \"User Not Found\"}, 404\n        user.delete_from_db()\n        return {\"message\": \"User deleted.\"}, 200\n\n\nclass TokenRefresh(Resource):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "project/using-flask-restx/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/using-flask-restx/Flask-JWT-Extended.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"74a1833f-bc4e-4e85-a525-72d268ab9999\",\n\t\t\"name\": \"Flask-JWT-Extended\",\n\t\t\"description\": \"This collection contains requests associated witht the Flask-JWT-Extended section of the REST API course.\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"users\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"register a new user\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/register\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"register\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/1\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"1\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/2\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"2\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"login\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"8c0c0ed6-c206-4c88-9349-429e024e312b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"refresh token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.refresh_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\",\n\t\t\t\t\t\t\t\t\t\"// set refresh token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.refresh_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"jose\\\",\\n  \\\"password\\\" : \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/login\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"login\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"logout\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"dc763e9b-e6c7-4ff3-9766-637976a5c64b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/logout\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"logout\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"refresh token\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"ad818ea6-8f79-436e-b756-ad878666ae9e\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{refresh_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/refresh\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"refresh\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"items\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"post item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"put item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete item by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items without JWT\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"stores\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"create a new store\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete a new store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all stores\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"stores\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "project/using-flask-restx/Stores_REST_API_2022-01-14.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-01-14T11:50:51.742Z\",\"__export_source\":\"insomnia.desktop.app:v2021.7.2\",\"resources\":[{\"_id\":\"req_efcadee1c4fc48f099644e23398a5d29\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159057139,\"created\":1642157007062,\"url\":\"{{url}}/register\",\"name\":\"/register\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_64c3752c7f694f0aa830bacba3b35aea\"},{\"name\":\"Authorization\",\"value\":\"JWT\",\"id\":\"pair_d143b36c4aa74f9681dc1590970da3b7\"}],\"authentication\":{},\"metaSortKey\":-1642157660252,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157670592,\"created\":1642157670592,\"name\":\"Authentication\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157670592,\"_type\":\"request_group\"},{\"_id\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"parentId\":null,\"modified\":1642157007080,\"created\":1642149963161,\"name\":\"Stores REST API\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_16415c75944342dab73119513e7bd20b\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159087108,\"created\":1642157007061,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_a8caec1064eb43b7ac5c8c9294be13a3\"}],\"authentication\":{},\"metaSortKey\":-1642157660202,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b08c51961bea4413a31fba1af93b3759\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642159093249,\"created\":1642157007054,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 3\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"},{\"id\":\"pair_4926e48dcb594eaa9c79a78b801b708f\",\"name\":\"Authorization\",\"value\":\"Bearer {% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always', 60 %}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157649712,\"created\":1642157649712,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157649712,\"_type\":\"request_group\"},{\"_id\":\"req_e553e5091f714becb81e1b27bfc8f34b\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642160992156,\"created\":1642157007053,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1f64b6c8fc8642aa9c267c8d49c72435\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157976127,\"created\":1642157007052,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_62498017fcb34ba0a3a19b4e0f2d4499\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_69d7ed86b4dc4b72a72778f97a77e05c\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157658823,\"created\":1642157007048,\"url\":\"{{url}}/item\",\"name\":\"/item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007178.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_054be716de114cc49d6e49d04a5a901b\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642159101503,\"created\":1642157684047,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"item_id\\\": {% response 'body', 'req_e553e5091f714becb81e1b27bfc8f34b', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_adac84f9834d4e948ceb02807787c935\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157684028,\"created\":1642157684028,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157641282.5,\"_type\":\"request_group\"},{\"_id\":\"req_a5b2631adf9e4ef894a8a1b9d2c77aa8\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158069229,\"created\":1642157684046,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"JWT \",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5dbef7a9b30d4ee68c148c4af447c241\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158073367,\"created\":1642157684041,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_7e8e3838e1cb41b485e091bf667b0764\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157645538,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320140.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157632853,\"created\":1642157632853,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157632853,\"_type\":\"request_group\"},{\"_id\":\"req_f99ce0192797434f99657221acc45fe3\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157643850,\"created\":1642157007059,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320090.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_82705a91a36849b09f1347d135816761\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157642682,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320040.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_94efb5c0488d43d8be95fd82b33afb97\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157639015,\"created\":1642157007060,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157319990.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9bc4db6f4d02466aba86edef29722854\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157781373,\"created\":1642157007070,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_8c8f8b7b9ddd4c3ca7fb6df5418b7f2e\"}],\"authentication\":{},\"metaSortKey\":-1642157007070,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157017423,\"created\":1642157007072,\"name\":\"User create store and item\",\"description\":\"Check user can register.\\nCheck user can create store.\\nCheck user can create item in store.\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157007128,\"_type\":\"request_group\"},{\"_id\":\"req_2ce4ecd840094ac1a164d7a0bfbb6d83\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157887543,\"created\":1642157007069,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007069,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8c68f13e77a74937b62cde1ae24bed61\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157923539,\"created\":1642157007068,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/test_item in test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": {% response 'body', 'req_2ce4ecd840094ac1a164d7a0bfbb6d83', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_d0c5451b8b044511b76436c627ffc4bb\"},{\"id\":\"pair_eb8ca7f686334ae5a48ad48412436ad9\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007068,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_297f6099ee274c1f8ccceb3bc29ad582\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007065,\"created\":1642157007065,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007065,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_84a807bef7cd4a66bc81f5401f0639cd\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157931016,\"created\":1642157007064,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/my_item copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_daca8133eb94474ca84748a0e4c8bcaf\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007064,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_10076b1332f2458e897d7b5200c7e5de\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007063,\"created\":1642157007063,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/<name> copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007063,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642160933906,\"created\":1642149963165,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5000\",\"access_token\":\"{% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'never', 60 %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1642149963165,\"_type\":\"environment\"},{\"_id\":\"jar_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963166,\"created\":1642149963166,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_f25b8aff2219447aa56189a385b1663c\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963162,\"created\":1642149963162,\"fileName\":\"Stores REST API\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "project/using-flask-restx/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_restx import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\nfrom resources.user import api as user_namespace\nfrom resources.item import api as item_namespace\nfrom resources.store import api as store_namespace\nfrom resources.tag import api as tag_namespace\n\napp = Flask(__name__)\napp.config[\"SQLALCHEMY_DATABASE_URI\"] = \"sqlite:///data.db\"\napp.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\ndb.init_app(app)\napi = Api(app)\n\n\"\"\"\nJWT related configuration. The following functions includes:\n1) add claims to each jwt\n2) customize the token expired error message\n\"\"\"\napp.config[\"JWT_SECRET_KEY\"] = \"jose\"\njwt = JWTManager(app)\n\n\"\"\"\n`claims` are data we choose to attach to each jwt payload\nand for each jwt protected endpoint, we can retrieve these claims via `get_jwt_claims()`\none possible use case for claims are access level control, which is shown below\n\"\"\"\n\n\n@jwt.additional_claims_loader\ndef add_claims_to_jwt(identity):\n    # TODO: Read from a config file instead of hard-coding\n    if identity == 1:\n        return {\"is_admin\": True}\n    return {\"is_admin\": False}\n\n\n@jwt.token_in_blocklist_loader\ndef check_if_token_in_blocklist(jwt_header, jwt_payload):\n    return jwt_payload[\"jti\"] in BLOCKLIST\n\n\n@jwt.expired_token_loader\ndef expired_token_callback(jwt_header, jwt_payload):\n    return jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}), 401\n\n\n@jwt.invalid_token_loader\ndef invalid_token_callback(error):\n    return (\n        jsonify(\n            {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n        ),\n        401,\n    )\n\n\n@jwt.unauthorized_loader\ndef missing_token_callback(error):\n    return (\n        jsonify(\n            {\n                \"description\": \"Request does not contain an access token.\",\n                \"error\": \"authorization_required\",\n            }\n        ),\n        401,\n    )\n\n\n@jwt.needs_fresh_token_loader\ndef token_not_fresh_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token is not fresh.\", \"error\": \"fresh_token_required\"}\n        ),\n        401,\n    )\n\n\n@jwt.revoked_token_loader\ndef revoked_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n        ),\n        401,\n    )\n\n\n# JWT configuration ends\n\n\nwith app.app_context():\n    import models  # noqa: F401\n\n    db.create_all()\n\n\napi.add_namespace(user_namespace, path=\"/\")\napi.add_namespace(item_namespace, path=\"/item\")\napi.add_namespace(store_namespace, path=\"/store\")\napi.add_namespace(tag_namespace, path=\"/tag\")\n"
  },
  {
    "path": "project/using-flask-restx/blocklist.py",
    "content": "\"\"\"\nblacklist.py\n\nThis file just contains the blacklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blacklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "project/using-flask-restx/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/using-flask-restx/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "project/using-flask-restx/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"price\": self.price,\n            \"store_id\": self.store_id,\n            \"tags\": [tag.json() for tag in self.tags],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restx/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "project/using-flask-restx/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.json() for item in self.items.all()],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restx/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.name for item in self.items],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restx/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = 'users'\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n\n    def json(self):\n        return {\n            'id': self.id,\n            'username': self.username\n        }\n\n    @classmethod\n    def find_by_username(cls, username):\n        return cls.query.filter_by(username=username).first()\n\n    @classmethod\n    def find_by_id(cls, _id):\n        return cls.query.filter_by(id=_id).first()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-restx/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-RESTX\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv"
  },
  {
    "path": "project/using-flask-restx/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "project/using-flask-restx/resources/item.py",
    "content": "from flask import request\nfrom flask_restx import Namespace, Resource, fields, abort\nfrom flask_jwt_extended import jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import ItemModel\n\napi = Namespace(\"items\", description=\"Operations related to store items.\")\n\nitem_inputs = api.model(\n    \"ItemFields\",\n    {\n        \"price\": fields.Float(required=True, description=\"A price for this item.\"),\n        \"store_id\": fields.Integer(\n            required=True,\n            description=\"The identifier for the store that this item belongs to.\",\n        ),\n    },\n)\n\nnested_resource = api.model(\n    \"NestedResource\", {\"id\": fields.Integer(), \"name\": fields.String()}\n)\n\nitem_outputs = api.inherit(\n    \"Item\",\n    item_inputs,\n    {\n        \"id\": fields.Integer(),\n        \"name\": fields.String(),\n        \"store\": fields.Nested(nested_resource),\n        \"tags\": fields.List(fields.Nested(nested_resource)),\n    },\n)\n\n\n@api.route(\"/<name>\")\n@api.param(\"name\", \"The unique name for the item you want to interact with.\")\n@api.doc(\n    responses={\n        404: \"Item not found.\",\n        400: \"Bad request (name already exists or validation error).\",\n        500: \"An error occurred while inserting that item.\",\n    }\n)\nclass Item(Resource):\n    @jwt_required()\n    @api.marshal_with(item_outputs)\n    def get(self, name):\n        item = ItemModel.find_by_name(name)\n        if item:\n            return item\n        abort(404, \"Item not found\")\n\n    @jwt_required(fresh=True)\n    @api.expect(item_inputs, validate=True)\n    @api.marshal_with(item_outputs)\n    def post(self, name):\n        if ItemModel.find_by_name(name):\n            abort(400, f\"An item with name {name} already exists.\")\n\n        item = ItemModel(name=name, **request.get_json())\n\n        try:\n            item.save_to_db()\n        except SQLAlchemyError:\n            abort(500, \"An error occurred while inserting the item.\")\n\n        return item, 201\n\n    @jwt_required()\n    def delete(self, name):\n        jwt = get_jwt()\n        if not jwt[\"is_admin\"]:\n            abort(401, \"Admin privilege required.\")\n\n        item = ItemModel.find_by_name(name)\n        if item:\n            item.delete_from_db()\n            return {\"message\": \"Item deleted.\"}\n        abort(404, \"Item not found.\")\n\n    @api.expect(item_inputs, validate=True)\n    @api.marshal_with(item_outputs)\n    def put(self, name):\n        item = ItemModel.find_by_name(name)\n\n        if item:\n            item.price = request.get_json()[\"price\"]\n        else:\n            item = ItemModel(name, **request.get_json())\n\n        item.save_to_db()\n        return item\n\n\n@api.route(\"/\")\nclass ItemList(Resource):\n    @api.marshal_list_with(item_outputs)\n    def get(self):\n        items = ItemModel.find_all()\n        return items, 200\n"
  },
  {
    "path": "project/using-flask-restx/resources/store.py",
    "content": "from flask import abort\nfrom flask_restx import Namespace, Resource, fields\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import StoreModel\n\n\napi = Namespace(\"stores\", description=\"Operations related to stores.\")\n\nnested_item = api.model(\n    \"NestedItem\",\n    {\n        \"id\": fields.Integer(),\n        \"name\": fields.String(),\n        \"price\": fields.Float(),\n    },\n)\n\nstore_outputs = api.model(\n    \"Store\",\n    {\n        \"id\": fields.Integer(),\n        \"name\": fields.String(),\n        \"items\": fields.List(fields.Nested(nested_item)),\n    },\n)\n\n\n@api.route(\"/<name>\")\nclass Store(Resource):\n    @api.marshal_with(store_outputs)\n    def get(self, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            return store\n        abort(404, \"Store not found.\")\n\n    @api.marshal_with(store_outputs)\n    def post(self, name):\n        if StoreModel.find_by_name(name):\n            abort(400, f\"A store with name '{name}' already exists.\")\n\n        store = StoreModel(name=name)\n        try:\n            store.save_to_db()\n        except SQLAlchemyError:\n            abort(500, \"An error occurred creating the store.\")\n\n        return store, 201\n\n    def delete(self, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            store.delete_from_db()\n            return {\"message\": \"Store deleted\"}, 200\n        abort(404, \"Store not found.\")\n\n\n@api.route(\"/\")\nclass StoreList(Resource):\n    @api.marshal_list_with(store_outputs)\n    def get(self):\n        return StoreModel.find_all()\n"
  },
  {
    "path": "project/using-flask-restx/resources/tag.py",
    "content": "from flask import abort, request\nfrom flask_restx import Namespace, Resource, fields\nfrom werkzeug.exceptions import BadRequest\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import TagModel\nfrom models import ItemModel\n\napi = Namespace(\n    \"tags\", description=\"Operations related to tags and their relationship to items.\"\n)\n\nitem_id = api.model(\"ItemId\", {\"item_id\": fields.Integer()})\n\nnested_item = api.inherit(\n    \"NestedItem\",\n    item_id,\n    {\n        \"name\": fields.String(),\n        \"price\": fields.Float(),\n    },\n)\n\nnested_tag = api.model(\n    \"NestedTag\",\n    {\n        \"id\": fields.Integer(),\n        \"name\": fields.String(),\n    },\n)\n\ntag_outputs = api.inherit(\n    \"Tag\",\n    nested_tag,\n    {\n        \"items\": fields.List(fields.Nested(nested_item)),\n    },\n)\n\n\n@api.route(\"/<name>\")\nclass Tag(Resource):\n    @api.marshal_with(tag_outputs)\n    def get(self, name):\n        tag = TagModel.find_by_name(name)\n        if tag:\n            return tag\n        abort(404, \"Tag not found.\")\n\n    @api.marshal_with(tag_outputs)\n    def post(self, name):\n        json_input = request.get_json()\n        tag = TagModel.find_by_name(name)\n        if not tag:\n            tag = TagModel(name=name)\n\n        # Add the item to the tag\n        try:\n            item = ItemModel.query.get(json_input[\"item_id\"])\n\n            if not item:\n                abort(400, \"An item with this item_id doesn't exist.\")\n\n            tag.items.append(item)\n        except (TypeError, KeyError):\n            abort(400, \"Missing required field 'item_id' in JSON body.\")\n\n        try:\n            tag.save_to_db()\n        except SQLAlchemyError:\n            abort(500, \"An error occurred while inserting the tag.\")\n\n        return tag, 201\n\n    def delete(self, name):\n        tag = TagModel.find_by_name(name)\n        if not tag:\n            abort(404, \"Tag not found.\")\n\n        if not tag.items:\n            tag.delete_from_db()\n            return {\"message\": f\"Tag '{name}' deleted.\"}\n        abort(\n            400,\n            \"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n        )\n\n\n@api.route(\"/<name>/remove\")\nclass RemoveItemFromTag(Resource):\n    @api.expect(item_id, validate=True)\n    def delete(self, name):\n        tag = TagModel.find_by_name(name)\n        if not tag:\n            abort(404, \"Tag not found.\")\n\n        try:\n            item_id = request.get_json()[\"item_id\"]\n            item = ItemModel.query.get(item_id)\n            try:\n                tag.items.remove(item)\n            except ValueError:\n                abort(\n                    400,\n                    f\"Could not remove item with id '{item_id}' from tag.\"\n                    \"Make sure item is associated with that item.\",\n                )\n            return {\"message\": f\"Item with id '{item_id}' removed from tag.\"}\n        except BadRequest:\n            abort(\n                400,\n                \"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n            )\n\n\n@api.route(\"/\")\nclass TagList(Resource):\n    @api.marshal_list_with(tag_outputs)\n    def get(self):\n        return TagModel.find_all()\n"
  },
  {
    "path": "project/using-flask-restx/resources/user.py",
    "content": "from flask import abort, request\nfrom flask_restx import Namespace, Resource, fields\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom models import UserModel\nfrom blocklist import BLOCKLIST\n\napi = Namespace(\"users\", description=\"Operations related to users and authentication.\")\n\nuser_inputs = api.model(\n    \"UserFields\",\n    {\n        \"username\": fields.String(required=True),\n        \"password\": fields.String(required=True),\n    },\n)\n\nuser_outputs = api.model(\"User\", {\"id\": fields.String(), \"username\": fields.String()})\n\n\n@api.route(\"/register\")\nclass UserRegister(Resource):\n    @api.expect(user_inputs, validate=True)\n    def post(self):\n        user_data = request.get_json()\n\n        if UserModel.find_by_username(user_data[\"username\"]):\n            abort(400, \"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        user.save_to_db()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@api.route(\"/login\")\nclass UserLogin(Resource):\n    @api.expect(user_inputs, validate=True)\n    def post(self):\n        user_data = request.get_json()\n\n        user = UserModel.find_by_username(user_data[\"username\"])\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, \"Invalid credentials.\")\n\n\n@api.route(\"/logout\")\nclass UserLogout(Resource):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@api.route(\"/user/<user_id>\")\nclass User(Resource):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @api.marshal_with(user_outputs)\n    def get(cls, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            abort(404, \"User not found.\")\n        return user, 200\n\n    def delete(self, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            abort(404, \"User not found.\")\n        user.delete_from_db()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@api.route(\"/refresh\")\nclass TokenRefresh(Resource):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "project/using-flask-smorest/.flaskenv",
    "content": "FLASK_APP=app\nFLASK_DEBUG=True"
  },
  {
    "path": "project/using-flask-smorest/Flask-JWT-Extended.postman_collection.json",
    "content": "{\n\t\"info\": {\n\t\t\"_postman_id\": \"74a1833f-bc4e-4e85-a525-72d268ab9999\",\n\t\t\"name\": \"Flask-JWT-Extended\",\n\t\t\"description\": \"This collection contains requests associated witht the Flask-JWT-Extended section of the REST API course.\",\n\t\t\"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n\t},\n\t\"item\": [\n\t\t{\n\t\t\t\"name\": \"users\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"register a new user\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/register\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"register\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/1\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"1\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete user by id\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n\\t\\\"username\\\": \\\"jose\\\",\\n\\t\\\"password\\\": \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/user/2\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"user\",\n\t\t\t\t\t\t\t\t\"2\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"login\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"8c0c0ed6-c206-4c88-9349-429e024e312b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"refresh token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.refresh_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\",\n\t\t\t\t\t\t\t\t\t\"// set refresh token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.refresh_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", jsonData.refresh_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"refresh_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"jose\\\",\\n  \\\"password\\\" : \\\"1234\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/login\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"login\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"logout\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"dc763e9b-e6c7-4ff3-9766-637976a5c64b\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/logout\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"logout\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"refresh token\",\n\t\t\t\t\t\"event\": [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\"listen\": \"test\",\n\t\t\t\t\t\t\t\"script\": {\n\t\t\t\t\t\t\t\t\"id\": \"ad818ea6-8f79-436e-b756-ad878666ae9e\",\n\t\t\t\t\t\t\t\t\"type\": \"text/javascript\",\n\t\t\t\t\t\t\t\t\"exec\": [\n\t\t\t\t\t\t\t\t\t\"var jsonData = pm.response.json();\",\n\t\t\t\t\t\t\t\t\t\"pm.test(\\\"access_token not empty\\\", function () {\",\n\t\t\t\t\t\t\t\t\t\"    pm.expect(jsonData.access_token).not.eql(undefined);\",\n\t\t\t\t\t\t\t\t\t\"});\",\n\t\t\t\t\t\t\t\t\t\"// set access token as environement variable\",\n\t\t\t\t\t\t\t\t\t\"if (jsonData.access_token !== undefined) {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", jsonData.access_token);\",\n\t\t\t\t\t\t\t\t\t\"} else {\",\n\t\t\t\t\t\t\t\t\t\"    postman.setEnvironmentVariable(\\\"access_token\\\", null);\",\n\t\t\t\t\t\t\t\t\t\"}\"\n\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t],\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{refresh_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/refresh\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"refresh\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"items\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"post item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"put item/name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"PUT\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"price\\\": 12.99,\\n  \\\"store_id\\\": 1\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete item by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Content-Type\",\n\t\t\t\t\t\t\t\t\"value\": \"application/json\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item/chair\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\t\"chair\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\"key\": \"Authorization\",\n\t\t\t\t\t\t\t\t\"value\": \"Bearer {{access_token}}\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all items without JWT\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {\n\t\t\t\t\t\t\t\"mode\": \"raw\",\n\t\t\t\t\t\t\t\"raw\": \"{\\n  \\\"username\\\" : \\\"cristiano\\\",\\n  \\\"password\\\" : \\\"12345678\\\"\\n}\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{local_flask}}/item\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{local_flask}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"items\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"name\": \"stores\",\n\t\t\t\"description\": \"\",\n\t\t\t\"item\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"create a new store\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"POST\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"delete a new store by name\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"DELETE\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store/My Wonderful Store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"store\",\n\t\t\t\t\t\t\t\t\"My Wonderful Store\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"get all stores\",\n\t\t\t\t\t\"request\": {\n\t\t\t\t\t\t\"method\": \"GET\",\n\t\t\t\t\t\t\"header\": [],\n\t\t\t\t\t\t\"body\": {},\n\t\t\t\t\t\t\"url\": {\n\t\t\t\t\t\t\t\"raw\": \"{{server_address}}/store\",\n\t\t\t\t\t\t\t\"host\": [\n\t\t\t\t\t\t\t\t\"{{server_address}}\"\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\"path\": [\n\t\t\t\t\t\t\t\t\"stores\"\n\t\t\t\t\t\t\t]\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t\"response\": []\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "project/using-flask-smorest/Stores_REST_API_2022-01-14.json",
    "content": "{\"_type\":\"export\",\"__export_format\":4,\"__export_date\":\"2022-01-14T11:50:51.742Z\",\"__export_source\":\"insomnia.desktop.app:v2021.7.2\",\"resources\":[{\"_id\":\"req_efcadee1c4fc48f099644e23398a5d29\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159057139,\"created\":1642157007062,\"url\":\"{{url}}/register\",\"name\":\"/register\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_64c3752c7f694f0aa830bacba3b35aea\"},{\"name\":\"Authorization\",\"value\":\"JWT\",\"id\":\"pair_d143b36c4aa74f9681dc1590970da3b7\"}],\"authentication\":{},\"metaSortKey\":-1642157660252,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157670592,\"created\":1642157670592,\"name\":\"Authentication\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157670592,\"_type\":\"request_group\"},{\"_id\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"parentId\":null,\"modified\":1642157007080,\"created\":1642149963161,\"name\":\"Stores REST API\",\"description\":\"\",\"scope\":\"collection\",\"_type\":\"workspace\"},{\"_id\":\"req_16415c75944342dab73119513e7bd20b\",\"parentId\":\"fld_fd1f956aae16470fafdc3d611d34a80a\",\"modified\":1642159087108,\"created\":1642157007061,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_a8caec1064eb43b7ac5c8c9294be13a3\"}],\"authentication\":{},\"metaSortKey\":-1642157660202,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_b08c51961bea4413a31fba1af93b3759\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642159093249,\"created\":1642157007054,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": 3\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"},{\"id\":\"pair_4926e48dcb594eaa9c79a78b801b708f\",\"name\":\"Authorization\",\"value\":\"Bearer {% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'always', 60 %}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157649712,\"created\":1642157649712,\"name\":\"Items\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157649712,\"_type\":\"request_group\"},{\"_id\":\"req_e553e5091f714becb81e1b27bfc8f34b\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642160992156,\"created\":1642157007053,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_1f64b6c8fc8642aa9c267c8d49c72435\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157976127,\"created\":1642157007052,\"url\":\"{{url}}/item/my_item\",\"name\":\"/item/my_item\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_62498017fcb34ba0a3a19b4e0f2d4499\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_69d7ed86b4dc4b72a72778f97a77e05c\",\"parentId\":\"fld_5bf669c32c3145a3a80dee2d6523f9ac\",\"modified\":1642157658823,\"created\":1642157007048,\"url\":\"{{url}}/item\",\"name\":\"/item\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007178.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_054be716de114cc49d6e49d04a5a901b\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642159101503,\"created\":1642157684047,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"item_id\\\": {% response 'body', 'req_e553e5091f714becb81e1b27bfc8f34b', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_9ded5c5a9c7e452386a15e8cc29bdcab\"}],\"authentication\":{},\"metaSortKey\":-1642157007278.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_adac84f9834d4e948ceb02807787c935\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157684028,\"created\":1642157684028,\"name\":\"Tags\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157641282.5,\"_type\":\"request_group\"},{\"_id\":\"req_a5b2631adf9e4ef894a8a1b9d2c77aa8\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158069229,\"created\":1642157684046,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[{\"name\":\"Authorization\",\"value\":\"JWT \",\"id\":\"pair_593ef7235a0d4f73b2fd09bd50f6c0c7\"}],\"authentication\":{},\"metaSortKey\":-1642157007253.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_5dbef7a9b30d4ee68c148c4af447c241\",\"parentId\":\"fld_adac84f9834d4e948ceb02807787c935\",\"modified\":1642158073367,\"created\":1642157684041,\"url\":\"{{url}}/tag/my_tag\",\"name\":\"/tag/my_tag\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007228.3906,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_7e8e3838e1cb41b485e091bf667b0764\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157645538,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320140.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157632853,\"created\":1642157632853,\"name\":\"Stores\",\"description\":\"\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157632853,\"_type\":\"request_group\"},{\"_id\":\"req_f99ce0192797434f99657221acc45fe3\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157643850,\"created\":1642157007059,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320090.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_82705a91a36849b09f1347d135816761\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157642682,\"created\":1642157007056,\"url\":\"{{url}}/store/my_store\",\"name\":\"/store/<name>\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157320040.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_94efb5c0488d43d8be95fd82b33afb97\",\"parentId\":\"fld_d76a6a294c734ea9bbc6fedf33cc29eb\",\"modified\":1642157639015,\"created\":1642157007060,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157319990.5,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_9bc4db6f4d02466aba86edef29722854\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157781373,\"created\":1642157007070,\"url\":\"{{url}}/login\",\"name\":\"/auth\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"username\\\": \\\"user1\\\",\\n\\t\\\"password\\\": \\\"abcxyz\\\"\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_8c8f8b7b9ddd4c3ca7fb6df5418b7f2e\"}],\"authentication\":{},\"metaSortKey\":-1642157007070,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642157017423,\"created\":1642157007072,\"name\":\"User create store and item\",\"description\":\"Check user can register.\\nCheck user can create store.\\nCheck user can create item in store.\",\"environment\":{},\"environmentPropertyOrder\":null,\"metaSortKey\":-1642157007128,\"_type\":\"request_group\"},{\"_id\":\"req_2ce4ecd840094ac1a164d7a0bfbb6d83\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157887543,\"created\":1642157007069,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007069,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_8c68f13e77a74937b62cde1ae24bed61\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157923539,\"created\":1642157007068,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/test_item in test_store\",\"description\":\"\",\"method\":\"POST\",\"body\":{\"mimeType\":\"\",\"text\":\"{\\n\\t\\\"price\\\": 17.99,\\n\\t\\\"store_id\\\": {% response 'body', 'req_2ce4ecd840094ac1a164d7a0bfbb6d83', 'b64::JC5pZA==::46b', 'never', 60 %}\\n}\"},\"parameters\":[],\"headers\":[{\"name\":\"Content-Type\",\"value\":\"application/json\",\"id\":\"pair_d0c5451b8b044511b76436c627ffc4bb\"},{\"id\":\"pair_eb8ca7f686334ae5a48ad48412436ad9\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007068,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_297f6099ee274c1f8ccceb3bc29ad582\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007065,\"created\":1642157007065,\"url\":\"{{url}}/store\",\"name\":\"/store\",\"description\":\"\",\"method\":\"GET\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007065,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_84a807bef7cd4a66bc81f5401f0639cd\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157931016,\"created\":1642157007064,\"url\":\"{{url}}/item/test_item\",\"name\":\"/item/my_item copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[{\"id\":\"pair_daca8133eb94474ca84748a0e4c8bcaf\",\"name\":\"Authorization\",\"value\":\"Bearer {{access_token}}\",\"description\":\"\"}],\"authentication\":{},\"metaSortKey\":-1642157007064,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"req_10076b1332f2458e897d7b5200c7e5de\",\"parentId\":\"fld_05fce0fdc405492d8a7f87842e6d4e13\",\"modified\":1642157007063,\"created\":1642157007063,\"url\":\"{{url}}/store/test_store\",\"name\":\"/store/<name> copy\",\"description\":\"\",\"method\":\"DELETE\",\"body\":{},\"parameters\":[],\"headers\":[],\"authentication\":{},\"metaSortKey\":-1642157007063,\"isPrivate\":false,\"settingStoreCookies\":true,\"settingSendCookies\":true,\"settingDisableRenderRequestBody\":false,\"settingEncodeUrl\":true,\"settingRebuildPath\":true,\"settingFollowRedirects\":\"global\",\"_type\":\"request\"},{\"_id\":\"env_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642160933906,\"created\":1642149963165,\"name\":\"Base Environment\",\"data\":{\"url\":\"http://127.0.0.1:5000\",\"access_token\":\"{% response 'body', 'req_16415c75944342dab73119513e7bd20b', 'b64::JC5hY2Nlc3NfdG9rZW4=::46b', 'never', 60 %}\"},\"dataPropertyOrder\":{\"&\":[\"url\",\"access_token\"]},\"color\":null,\"isPrivate\":false,\"metaSortKey\":1642149963165,\"_type\":\"environment\"},{\"_id\":\"jar_34cb01359e95568602d0f3f1a1c4d42a45b00dc5\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963166,\"created\":1642149963166,\"name\":\"Default Jar\",\"cookies\":[],\"_type\":\"cookie_jar\"},{\"_id\":\"spc_f25b8aff2219447aa56189a385b1663c\",\"parentId\":\"wrk_c441bf446d174d1bb2f01c7ad66c695b\",\"modified\":1642149963162,\"created\":1642149963162,\"fileName\":\"Stores REST API\",\"contents\":\"\",\"contentType\":\"yaml\",\"_type\":\"api_spec\"}]}"
  },
  {
    "path": "project/using-flask-smorest/app.py",
    "content": "from flask import Flask, jsonify\nfrom flask_smorest import Api\nfrom flask_jwt_extended import JWTManager\n\nfrom db import db\nfrom blocklist import BLOCKLIST\n\nfrom resources.user import blp as UserBlueprint\nfrom resources.item import blp as ItemBlueprint\nfrom resources.store import blp as StoreBlueprint\nfrom resources.tag import blp as TagBlueprint\n\n\napp = Flask(__name__)\napp.config[\"API_TITLE\"] = \"Stores REST API\"\napp.config[\"API_VERSION\"] = \"v1\"\napp.config[\"OPENAPI_VERSION\"] = \"3.0.3\"\napp.config[\"OPENAPI_URL_PREFIX\"] = \"/\"\napp.config[\"OPENAPI_SWAGGER_UI_PATH\"] = \"/swagger-ui\"\napp.config[\"OPENAPI_SWAGGER_UI_URL\"] = \"https://cdn.jsdelivr.net/npm/swagger-ui-dist/\"\napp.config[\"SQLALCHEMY_DATABASE_URI\"] = \"sqlite:///data.db\"\napp.config[\"SQLALCHEMY_TRACK_MODIFICATIONS\"] = False\napp.config[\"PROPAGATE_EXCEPTIONS\"] = True\ndb.init_app(app)\napi = Api(app)\n\n\"\"\"\nJWT related configuration. The following functions includes:\n1) add claims to each jwt\n2) customize the token expired error message\n\"\"\"\napp.config[\"JWT_SECRET_KEY\"] = \"jose\"\njwt = JWTManager(app)\n\n\"\"\"\n`claims` are data we choose to attach to each jwt payload\nand for each jwt protected endpoint, we can retrieve these claims via `get_jwt_claims()`\none possible use case for claims are access level control, which is shown below\n\"\"\"\n\n\n@jwt.additional_claims_loader\ndef add_claims_to_jwt(identity):\n    # TODO: Read from a config file instead of hard-coding\n    if identity == 1:\n        return {\"is_admin\": True}\n    return {\"is_admin\": False}\n\n\n@jwt.token_in_blocklist_loader\ndef check_if_token_in_blocklist(jwt_header, jwt_payload):\n    return jwt_payload[\"jti\"] in BLOCKLIST\n\n\n@jwt.expired_token_loader\ndef expired_token_callback(jwt_header, jwt_payload):\n    return jsonify({\"message\": \"The token has expired.\", \"error\": \"token_expired\"}), 401\n\n\n@jwt.invalid_token_loader\ndef invalid_token_callback(error):\n    return (\n        jsonify(\n            {\"message\": \"Signature verification failed.\", \"error\": \"invalid_token\"}\n        ),\n        401,\n    )\n\n\n@jwt.unauthorized_loader\ndef missing_token_callback(error):\n    return (\n        jsonify(\n            {\n                \"description\": \"Request does not contain an access token.\",\n                \"error\": \"authorization_required\",\n            }\n        ),\n        401,\n    )\n\n\n@jwt.needs_fresh_token_loader\ndef token_not_fresh_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token is not fresh.\", \"error\": \"fresh_token_required\"}\n        ),\n        401,\n    )\n\n\n@jwt.revoked_token_loader\ndef revoked_token_callback(jwt_header, jwt_payload):\n    return (\n        jsonify(\n            {\"description\": \"The token has been revoked.\", \"error\": \"token_revoked\"}\n        ),\n        401,\n    )\n\n\n# JWT configuration ends\n\n\nwith app.app_context():\n    import models  # noqa: F401\n\n    db.create_all()\n\n\napi.register_blueprint(UserBlueprint)\napi.register_blueprint(ItemBlueprint)\napi.register_blueprint(StoreBlueprint)\napi.register_blueprint(TagBlueprint)\n"
  },
  {
    "path": "project/using-flask-smorest/blocklist.py",
    "content": "\"\"\"\nblacklist.py\n\nThis file just contains the blacklist of the JWT tokens. It will be imported by\napp and the logout resource so that tokens can be added to the blacklist when the\nuser logs out.\n\"\"\"\n\nBLOCKLIST = set()\n"
  },
  {
    "path": "project/using-flask-smorest/db.py",
    "content": "from flask_sqlalchemy import SQLAlchemy\n\ndb = SQLAlchemy()\n"
  },
  {
    "path": "project/using-flask-smorest/models/__init__.py",
    "content": "from models.user import UserModel\nfrom models.item import ItemModel\nfrom models.tag import TagModel\nfrom models.store import StoreModel\nfrom models.item_tags import ItemsTags\n"
  },
  {
    "path": "project/using-flask-smorest/models/item.py",
    "content": "from db import db\n\n\nclass ItemModel(db.Model):\n    __tablename__ = \"items\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    price = db.Column(db.Float(precision=2), unique=False, nullable=False)\n\n    store_id = db.Column(\n        db.Integer, db.ForeignKey(\"stores.id\"), unique=False, nullable=False\n    )\n    store = db.relationship(\"StoreModel\", back_populates=\"items\")\n\n    tags = db.relationship(\"TagModel\", back_populates=\"items\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"price\": self.price,\n            \"store_id\": self.store_id,\n            \"tags\": [tag.json() for tag in self.tags],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-smorest/models/item_tags.py",
    "content": "from db import db\n\n\nclass ItemsTags(db.Model):\n    __tablename__ = \"items_tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    item_id = db.Column(db.Integer, db.ForeignKey(\"items.id\"))\n    tag_id = db.Column(db.Integer, db.ForeignKey(\"tags.id\"))\n"
  },
  {
    "path": "project/using-flask-smorest/models/store.py",
    "content": "from db import db\n\n\nclass StoreModel(db.Model):\n    __tablename__ = \"stores\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=True, nullable=False)\n\n    items = db.relationship(\"ItemModel\", back_populates=\"store\", lazy=\"dynamic\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.json() for item in self.items.all()],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-smorest/models/tag.py",
    "content": "from db import db\n\n\nclass TagModel(db.Model):\n    __tablename__ = \"tags\"\n\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(80), unique=False, nullable=False)\n    items = db.relationship(\"ItemModel\", back_populates=\"tags\", secondary=\"items_tags\")\n\n    def json(self):\n        return {\n            \"id\": self.id,\n            \"name\": self.name,\n            \"items\": [item.name for item in self.items],\n        }\n\n    @classmethod\n    def find_by_name(cls, name):\n        return cls.query.filter_by(name=name).first()\n\n    @classmethod\n    def find_all(cls):\n        return cls.query.all()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-smorest/models/user.py",
    "content": "from db import db\n\n\nclass UserModel(db.Model):\n    __tablename__ = 'users'\n\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(80), unique=True, nullable=False)\n    password = db.Column(db.String(80), nullable=False)\n\n    def json(self):\n        return {\n            'id': self.id,\n            'username': self.username\n        }\n\n    @classmethod\n    def find_by_username(cls, username):\n        return cls.query.filter_by(username=username).first()\n\n    @classmethod\n    def find_by_id(cls, _id):\n        return cls.query.filter_by(id=_id).first()\n\n    def save_to_db(self):\n        db.session.add(self)\n        db.session.commit()\n\n    def delete_from_db(self):\n        db.session.delete(self)\n        db.session.commit()\n"
  },
  {
    "path": "project/using-flask-smorest/requirements.txt",
    "content": "Flask-JWT-Extended\nFlask-Smorest\nFlask-SQLAlchemy\npasslib\nmarshmallow\npython-dotenv"
  },
  {
    "path": "project/using-flask-smorest/resources/__init__.py",
    "content": "\n"
  },
  {
    "path": "project/using-flask-smorest/resources/item.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import get_jwt_identity, jwt_required, get_jwt\nfrom sqlalchemy.exc import SQLAlchemyError\n\nfrom models import ItemModel\nfrom schemas import ItemSchema, ItemUpdateSchema\n\nblp = Blueprint(\"Items\", __name__, description=\"Operations on items\")\n\n\n@blp.route(\"/item/<string:name>\")\nclass Item(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema)\n    def get(self, name):\n        item = ItemModel.find_by_name(name)\n        if item:\n            return item\n        abort(404, message=\"Item not found\")\n\n    @jwt_required(fresh=True)\n    @blp.arguments(ItemSchema)\n    @blp.response(201, ItemSchema)\n    def post(self, item_data, name):\n        if ItemModel.find_by_name(name):\n            abort(400, message=f\"An item with name {name} already exists.\")\n\n        item = ItemModel(**item_data, name=name)\n\n        try:\n            item.save_to_db()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the item.\")\n\n        return item\n\n    @jwt_required()\n    def delete(self, name):\n        jwt = get_jwt()\n        if not jwt[\"is_admin\"]:\n            abort(401, message=\"Admin privilege required.\")\n\n        item = ItemModel.find_by_name(name)\n        if item:\n            item.delete_from_db()\n            return {\"message\": \"Item deleted.\"}\n        abort(404, message=\"Item not found.\")\n\n    @blp.arguments(ItemUpdateSchema)\n    @blp.response(200, ItemSchema)\n    def put(self, item_data, name):\n        item = ItemModel.find_by_name(name)\n\n        if item:\n            item.price = item_data[\"price\"]\n        else:\n            item = ItemModel(name, **item_data)\n\n        item.save_to_db()\n\n        return item\n\n\n@blp.route(\"/item\")\nclass ItemList(MethodView):\n    @jwt_required()\n    @blp.response(200, ItemSchema(many=True))\n    def get(self):\n        return ItemModel.find_all()\n"
  },
  {
    "path": "project/using-flask-smorest/resources/store.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import StoreModel\nfrom schemas import StoreSchema\n\n\nblp = Blueprint(\"Stores\", __name__, description=\"Operations on stores\")\n\n\n@blp.route(\"/store/<string:name>\")\nclass Store(MethodView):\n    @blp.response(200, StoreSchema)\n    def get(cls, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            return store\n        abort(404, message=\"Store not found.\")\n\n    @blp.response(201, StoreSchema)\n    def post(cls, name):\n        if StoreModel.find_by_name(name):\n            abort(400, message=f\"A store with name '{name}' already exists.\")\n\n        store = StoreModel(name=name)\n        try:\n            store.save_to_db()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred creating the store.\")\n\n        return store\n\n    def delete(cls, name):\n        store = StoreModel.find_by_name(name)\n        if store:\n            store.delete_from_db()\n            return {\"message\": \"Store deleted\"}, 200\n        abort(404, message=\"Store not found.\")\n\n\n@blp.route(\"/store\")\nclass StoreList(MethodView):\n    @blp.response(200, StoreSchema(many=True))\n    def get(cls):\n        return StoreModel.find_all()\n"
  },
  {
    "path": "project/using-flask-smorest/resources/tag.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom werkzeug.exceptions import BadRequest\nfrom sqlalchemy.exc import SQLAlchemyError\nfrom models import TagModel\nfrom models import ItemModel\nfrom schemas import TagSchema, TagUpdateSchema, TagAndItemSchema\n\nblp = Blueprint(\"Tags\", \"tags\", description=\"Operations on tags\")\n\n\n@blp.route(\"/tag/<string:name>\")\nclass Tag(MethodView):\n    @blp.response(200, TagSchema)\n    def get(self, name):\n        tag = TagModel.find_by_name(name)\n        if tag:\n            return tag\n        abort(404, message=\"Tag not found.\")\n\n    @blp.arguments(TagUpdateSchema)\n    @blp.response(201, TagSchema)\n    def post(self, update_data, name):\n        tag = TagModel.find_by_name(name)\n        if not tag:\n            tag = TagModel(name=name)\n\n        # Add the item to the tag\n        try:\n            item = ItemModel.query.get(update_data[\"item_id\"])\n\n            if not item:\n                abort(400, message=\"An item with this item_id doesn't exist.\")\n\n            tag.items.append(item)\n        except (TypeError, KeyError):\n            abort(400, message=\"Missing required field 'item_id' in JSON body.\")\n\n        try:\n            tag.save_to_db()\n        except SQLAlchemyError:\n            abort(500, message=\"An error occurred while inserting the tag.\")\n\n        return tag\n\n    @blp.arguments(TagUpdateSchema, required=False)\n    @blp.response(200, TagAndItemSchema)\n    @blp.alt_response(\n        202,\n        description=\"Deletes a tag when it has no items and no item_id is passed in the body.\",\n        example={\"message\": \"Tag deleted.\"},\n        success=True,\n    )\n    @blp.alt_response(404, description=\"Tag not found\")\n    @blp.alt_response(\n        400, description=\"Missing item_id in body when tag is associated to items.\"\n    )\n    def delete(self, tag_data, name):\n        \"\"\"Deletes a tag.\n\n        If the tag is associated to items, expects an item_id in the JSON body and unlinks the item from the tag.\n\n        If the tag is not associated to any items, then does not expect item_id in the JSON body and deletes the tag entirely.\n        \"\"\"\n        tag = TagModel.find_by_name(name)\n        if \"item_id\" in tag_data:\n            item = ItemModel.query.get(tag_data[\"item_id\"])\n            tag.items.remove(item)\n            tag.save_to_db()\n            return {\n                \"message\": \"Item removed from tag\",\n                \"item\": item,\n                \"tag\": tag,\n            }\n        else:\n            # Assume no item_id was passed. Instead delete entire tag.\n            # First check tag has no items\n            if not tag.items:\n                tag.delete_from_db()\n                return {\"message\": \"Tag deleted.\"}\n            abort(\n                400,\n                message=\"Could not delete tag. Make sure tag is not associated with any items, then try again.\",  # noqa: E501\n            )\n        abort(404, message=\"Tag not found.\")\n"
  },
  {
    "path": "project/using-flask-smorest/resources/user.py",
    "content": "from flask.views import MethodView\nfrom flask_smorest import Blueprint, abort\nfrom flask_jwt_extended import (\n    create_access_token,\n    create_refresh_token,\n    get_jwt_identity,\n    get_jwt,\n    jwt_required,\n)\nfrom passlib.hash import pbkdf2_sha256\n\nfrom models import UserModel\nfrom schemas import UserSchema\nfrom blocklist import BLOCKLIST\n\n\nblp = Blueprint(\"Users\", \"users\", description=\"Operations on users\")\n\n\n@blp.route(\"/register\")\nclass UserRegister(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        if UserModel.find_by_username(user_data[\"username\"]):\n            abort(400, message=\"A user with that username already exists.\")\n\n        user = UserModel(\n            username=user_data[\"username\"],\n            password=pbkdf2_sha256.hash(user_data[\"password\"]),\n        )\n        user.save_to_db()\n\n        return {\"message\": \"User created successfully.\"}, 201\n\n\n@blp.route(\"/login\")\nclass UserLogin(MethodView):\n    @blp.arguments(UserSchema)\n    def post(self, user_data):\n        user = UserModel.find_by_username(user_data[\"username\"])\n\n        if user and pbkdf2_sha256.verify(user_data[\"password\"], user.password):\n            access_token = create_access_token(identity=str(user.id), fresh=True)\n            refresh_token = create_refresh_token(user.id)\n            return {\"access_token\": access_token, \"refresh_token\": refresh_token}, 200\n\n        abort(401, message=\"Invalid credentials.\")\n\n\n@blp.route(\"/logout\")\nclass UserLogout(MethodView):\n    @jwt_required()\n    def post(self):\n        jti = get_jwt()[\"jti\"]\n        BLOCKLIST.add(jti)\n        return {\"message\": \"Successfully logged out\"}, 200\n\n\n@blp.route(\"/user/<int:user_id>\")\nclass User(MethodView):\n    \"\"\"\n    This resource can be useful when testing our Flask app.\n    We may not want to expose it to public users, but for the\n    sake of demonstration in this course, it can be useful\n    when we are manipulating data regarding the users.\n    \"\"\"\n\n    @blp.response(200, UserSchema)\n    def get(self, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            abort(404, message=\"User not found.\")\n        return user\n\n    def delete(self, user_id):\n        user = UserModel.find_by_id(user_id)\n        if not user:\n            abort(404, message=\"User not found.\")\n        user.delete_from_db()\n        return {\"message\": \"User deleted.\"}, 200\n\n\n@blp.route(\"/refresh\")\nclass TokenRefresh(MethodView):\n    @jwt_required(refresh=True)\n    def post(self):\n        current_user = get_jwt_identity()\n        new_token = create_access_token(identity=current_user, fresh=False)\n        return {\"access_token\": new_token}, 200\n"
  },
  {
    "path": "project/using-flask-smorest/schemas.py",
    "content": "from marshmallow import Schema, fields\n\n\nclass ItemSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True, dump_only=True)\n    price = fields.Float(required=True)\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(lambda: StoreWitoutItemsSchema(), dump_only=True)\n    tags = fields.List(fields.Nested(lambda: TagWithoutItemsSchema()), dump_only=True)\n\n\nclass ItemWithoutStoreSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True, dump_only=True)\n    price = fields.Float(required=True)\n    tags = fields.List(fields.Nested(lambda: TagWithoutItemsSchema()), dump_only=True)\n\n\nclass ItemWithoutTagsSchema(Schema):\n    id = fields.Int(dump_only=True)\n    name = fields.Str(required=True, dump_only=True)\n    price = fields.Float(required=True)\n    store_id = fields.Int(required=True, load_only=True)\n    store = fields.Nested(lambda: StoreWitoutItemsSchema(), dump_only=True)\n\n\nclass ItemUpdateSchema(Schema):\n    price = fields.Float(required=True)\n\n\nclass StoreSchema(Schema):\n    id = fields.Int()\n    name = fields.Str()\n    items = fields.List(fields.Nested(ItemWithoutStoreSchema()), dump_only=True)\n\n\nclass StoreWitoutItemsSchema(Schema):\n    id = fields.Int()\n    name = fields.Str()\n\n\nclass TagSchema(Schema):\n    id = fields.Int()\n    name = fields.Str()\n    items = fields.List(fields.Nested(ItemWithoutTagsSchema()), dump_only=True)\n\n\nclass TagWithoutItemsSchema(Schema):\n    id = fields.Int()\n    name = fields.Str()\n\n\nclass TagUpdateSchema(Schema):\n    item_id = fields.Int()\n\n\nclass TagAndItemSchema(Schema):\n    message = fields.Str()\n    item = fields.Nested(ItemSchema)\n    tag = fields.Nested(TagSchema)\n\n\nclass UserSchema(Schema):\n    id = fields.Int()\n    username = fields.Str()\n    password = fields.Str(load_only=True)\n"
  }
]