Skip to content

Commit

Permalink
docs: simplify Python tutorial (#6073)
Browse files Browse the repository at this point in the history
<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

- Reducing the amount of steps to implement the Python app feature flag
- fixed spaces & indentations in Python code snippets (mentioned in past
PR review)
- `git clone` my forked repo so we can customize that code in the future
as we improve it, without being blocked.


<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

More improvements to be made to the HTTP calls in `routes.py` as I
continue working on resolving some issues I'm running into in the app.

---------

Co-authored-by: Simon Hornby <liquidwicked64@gmail.com>
  • Loading branch information
nnennandukwe and sighphyre committed Jan 31, 2024
1 parent bb02ffd commit 542acd3
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 63 deletions.
Expand Up @@ -37,6 +37,9 @@ In this tutorial, you will need the following:

![An architectural diagram of our Python app using Unleash feature flags](/img/python-flask-unleash-architecture.png)

This architecture diagram breaks down how the Python app works with Unleash to control feature flags. We connect the Unleash service to your Python app using the Python SDK.

The Unleash Server is a **Feature Flag Control Service**, which is a service that manages your feature flags and is used to retrieve flag data from (and send data to, especially when not using a UI). The Unleash server has a UI for creating and managing projects and feature flags. There are also [API commands available](https://docs.getunleash.io/reference/api/unleash) to perform the same actions straight from your CLI or server-side app.

## 1. Unleash best practice for backend apps

Expand Down Expand Up @@ -130,7 +133,7 @@ In this section, you will clone an open-source Python application called [Flask
Use this command to clone the repository via your Terminal:

```
git clone git@github.com:pamelafox/flask-surveys-container-app.git
git clone git@github.com:nnennandukwe/flask-surveys-container-app.git
```

Next, navigate into your repository directory and create a `.env` file.
Expand All @@ -155,7 +158,7 @@ UnleashClient==5.11.1
```


In `src/backend/__init__.py`, import UnleashClient:
In `src/backend/__init__.py`, import `UnleashClient`:


```py
Expand All @@ -168,9 +171,10 @@ In the same file, call the Unleash client for initialization when the app runs w

```py
client = UnleashClient(
url="http://host.docker.internal:4242/api",
app_name="flask-surveys-container-app",
custom_headers={'Authorization': '<API token>'})
url="http://host.docker.internal:4242/api",
app_name="flask-surveys-container-app",
custom_headers={'Authorization': '<API token>'}
)

client.initialize_client()
```
Expand Down Expand Up @@ -209,14 +213,24 @@ This will require us to:
- Create a delete button
- Map the delete button to the delete method

In `src/backend/surveys/routes.py`, add `client` to the existing `backend` import statement. The full import line will now look like this:
First, we need an error handler to return a simple 404 page to stop a user from being able to delete a survey when the flag is off. We will use this function in our delete method.

In your `routes.py` file, import a module from Flask that will support error handling: [`abort`](https://flask.palletsprojects.com/en/3.0.x/api/#flask.abort).

Line 1 will now look like this:

```py
from flask import redirect, render_template, request, url_for, abort
```

Add `client` to the `backend` import statement on line 4. The full import line will now look like this:


```py
from backend import db, client
```

We’ve imported the initialized Unleash client into `routes.py`. Now we can use that data to pass into the `surveys_list_page` method. This will allow us to check the status of the enabled flag to conditionally render the 'Delete' button on the surveys page.
We’ve imported the initialized Unleash client into `routes.py`. Now we can use that data to pass into the `surveys_list_page` method. This will allow us to check the status of the enabled flag to conditionally render the delete button on the surveys page.

Add `client` as a parameter in the template that we return in the `surveys_list_page` method.

Expand All @@ -226,16 +240,21 @@ The modified return statement in this method will now look like this:
return render_template("surveys_list.html", surveys=surveys, client=client)
```

In the same file, we will create a new route and a delete method with this code snippet:
In the same file, we will create a new route and a delete method with this code snippet:

```py
@bp.route("/surveys/<int:survey_id>/delete", methods=["GET", "POST", "DELETE"])
def delete_survey(survey_id):
survey = db.get_or_404(Survey, survey_id)
db.session.delete(survey)
db.session.commit()
# if flag is not enabled, return a 404 page
if not client.is_enabled('delete_survey_flag'):
abort(404, description="Resource not found")
else:
# otherwise, delete the survey
survey = db.get_or_404(Survey, survey_id)
db.session.delete(survey)
db.session.commit()

return redirect(url_for("surveys.surveys_list_page"))
return redirect(url_for("surveys.surveys_list_page"))
```

The server now has a route that uses a survey ID to locate the survey in the database and delete it.
Expand All @@ -250,7 +269,7 @@ In `src/backend/templates/surveys_list.html`, add the following code to your sur
{% endif %}
```

This code wraps a delete button in a conditional statement that checks whether or not the feature flag is enabled. This button has a link that points to the `delete_survey` method we created, which will pull in the survey using an ID to search the database, find the matching survey, and delete it from the database session.
This code wraps a delete button in a conditional statement that checks whether or not the feature flag is enabled. This button has a link that points to the `delete_survey` method we created, which will pull in the survey using an ID to search the database, find the matching survey, and delete it from the session.

Your surveys page will now look something like this:

Expand All @@ -277,55 +296,7 @@ Next, return to your Survey app and refresh the browser. With the flag disabled,
![Screenshot of app in browser without delete buttons for surveys](/img/python-tutorial-surveys-without-delete.png)


## 7. Improve a feature flag implementation with error handling

If you turn the feature flag off, you won’t be able to use the delete button to remove a survey from your list. However, if a user wanted to bypass the UI to delete a survey, they could still use the URL from the delete method route to target a survey and delete it.

They could do this because we have committed code that is only _partially_ hidden behind a feature flag. The HTML code is behind the flag, but the server method that it talks to is not.

In a real world application, ignoring this would cause a user to perform an action they _shouldn’t_ able to. Luckily, we can use a feature flag to stop the delete method from being called manually.

Let’s walk through how to gracefully handle this scenario:

We need an error handler route to return a simple 404 page to stop a user from being able to manually delete a survey when the flag is off.

In your `routes.py` file, import two more modules from Flask that will support our error handling function: [`abort`](https://flask.palletsprojects.com/en/3.0.x/api/#flask.abort) and [`jsonify`](https://flask.palletsprojects.com/en/3.0.x/api/#flask.json.jsonify).

Line 1 will now look like this:

```py
from flask import redirect, render_template, request, url_for, abort, jsonify
```

Next, add in the following error handling method at the bottom of the file:

```py
@bp.errorhandler(404)
def resource_not_found(e):
return jsonify(error=str(e)), 404
```

In order to render the error message, we can call it from the `delete_survey` method only in the case that the feature flag is turned off. Here’s how the updated `delete_survey` code would look like:

```py
@bp.route("/surveys/<int:survey_id>/delete", methods=["GET", "POST", "DELETE"])
def delete_survey(survey_id):
if client.is_enabled('delete_survey_flag'):
survey = db.get_or_404(Survey, survey_id)
db.session.delete(survey)
db.session.commit()

return redirect(url_for("surveys.surveys_list_page"))
else:
abort(404, description="Resource not found")
```

Now, if you turn off the flag in your Unleash instance and attempt to delete a survey directly with a URL, the 404 error will return.

![Screenshot of 404 error rendering in browser](/img/python-tutorial-404.png)

Learn more about [Flask Blueprint error handling](https://flask.palletsprojects.com/en/3.0.x/errorhandling/#blueprint-error-handlers).

## Conclusion

In this tutorial, we installed Unleash locally, created a new feature flag, installed Unleash into a Python Flask app, and toggled new functionality that altered a database with a containerized project!

In this tutorial, we ran Unleash locally, created a new feature flag, installed the Python SDK into a Python Flask app, and toggled new functionality that altered a database with a containerized project!
Binary file removed website/static/img/python-tutorial-404.png
Binary file not shown.

0 comments on commit 542acd3

Please sign in to comment.