Skip to content

Commit

Permalink
Merge pull request #25 from lcdunne/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
lcdunne authored Sep 7, 2024
2 parents 3cb9e3a + 4cbf4a8 commit 95b6322
Show file tree
Hide file tree
Showing 24 changed files with 1,413 additions and 451 deletions.
51 changes: 50 additions & 1 deletion .github/workflows/quality-control.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,59 @@ jobs:
- name: Run tests
run: |
source venv/bin/activate
pytest --junitxml=report.xml
pytest -v -x --junitxml=report.xml
- name: Upload test report
uses: actions/upload-artifact@v3
with:
name: test-report
path: report.xml

check-version:
name: Check version tag 🏷️
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.base.ref == 'main' }}

steps:
- name: Checkout base branch and PR code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.base.ref }}
path: base

- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
path: pr

- name: Extract version from base branch pyproject.toml
id: extract_base_version
run: |
base_version=$(grep '^version =' base/pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "::set-output name=base_version::$base_version"
- name: Extract version from PR branch pyproject.toml
id: extract_pr_version
run: |
pr_version=$(grep '^version =' pr/pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "::set-output name=pr_version::$pr_version"
- name: Compare versions using Python
run: |
base_version="${{ steps.extract_base_version.outputs.base_version }}"
pr_version="${{ steps.extract_pr_version.outputs.pr_version }}"
python - <<EOF
import sys
from packaging import version
base_version = version.parse("$base_version")
pr_version = version.parse("$pr_version")
if pr_version <= base_version:
print(f"Invalid version bump ({pr_version} <= {base_version})")
sys.exit(1)
else:
print(f"Valid version bump ({pr_version} > {base_version})")
EOF
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# VSCode
.vscode
123 changes: 36 additions & 87 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Flask-Reqcheck

Validate requests to a flask server using Pydantic models.

## Motivation

The purpose of Flask-Reqcheck is to simply validate requests against a [Pydantic](https://docs.pydantic.dev/latest/) model. This is partly inspired by [Flask-Pydantic](https://github.com/bauerji/flask-pydantic), and supports Pydantic v2 and the latest version of Flask.
**Flask-Reqcheck** lets you validate requests in your Flask applications. With a simple
decorator and some [Pydantic](https://docs.pydantic.dev/latest/) models, you can quickly
validate incoming request bodies, query parameters, and url path parameters, reducing
boilerplate code and minimizing errors.

## Installation

Expand All @@ -14,15 +13,22 @@ Run the following (preferably inside a virtual environment):
pip install flask-reqcheck
```

For development, clone the repository and install it locally, along with the test dependencies:
## Usage

```sh
python -m pip install -e '.[dev]'
```
Flask-Reqcheck is very straightforward to use. The main two objects of interest are the `@validate` decorator and the `get_valid_request` function.

## Usage
The `validate` decorator is for annotating flask route functions. When you do this, you provide a Pydantic model for the components of the HTTP
request that you would like to validate, such as `body`, `query`, `path`, etc. If the request inputs fail to match the corresponding model then
a HTTP error is raised.

Here is an example of how to use this library:
Aside from `@validate`, you can use the more specific decorators - `@validate_body`, `@validate_form`, `@validate_path`,
`@validate_query`, etc (see the API reference).

The `get_valid_request` is a helper function for use *within* the Flask route function. When using `@validate`, a new instance of the `ValidRequest` class
will be created and stored for the current request context. We can use `get_valid_request` to retrieve that object and access its attributes, which correspond
to the different HTTP components that were validated.

For example:

```python
from flask_reqcheck import validate, get_valid_request
Expand All @@ -36,96 +42,39 @@ class BodyModel(BaseModel):
d: uuid.UUID
arr: list[int]


@app.post("/body")
@validate(body=BodyModel) # Decorate the view function
def request_with_body():
vreq = get_valid_request() # Access the validated data
return vreq.to_dict()
```

First, import the `validate` decorator function and the `get_valid_request` helper function. The `validate` decorator accepts arguments in the form of Pydantic model classes for the request body, query parameters, path (url) parameters, and form data (headers & cookies not yet implemented). The `get_valid_request` function provides access to a `ValidRequest` object that stores the validated request data in a single place. Using this we can easily access our validated data from that object in our route functions.

For a full example of how to use this, see `example/app.py`.

### Path parameters

Simply type-hinting the Flask route function arguments will result in those parameters being validated, and a Pydantic model is not required in this case:

```python

@app.get("/path/typed/<a>/<b>/<c>/<d>")
@validate() # A model is not required for the path parameters
def valid_path(a: str, b: int, c: float, d: uuid.UUID):
vreq = get_valid_request()
return vreq.as_dict()
```

If type hints are omitted from the route function signature then it just falls back to Flask's default - [converter types](https://flask.palletsprojects.com/en/3.0.x/quickstart/#variable-rules) (if provided in the path definition) or strings.

### Query parameters

Query parameters require you to write a Pydantic model that represents the query parameters expected for the route. For example:

```python
class QueryModel(BaseModel):
a: str | None = None
b: int | None = None
c: float | None = None
d: uuid.UUID | None = None
arr: list[int] | None = None
x: str

@app.get("/query")
@validate(query=QueryModel)
def request_with_query_parameters():
vreq = get_valid_request()
return vreq.to_dict()
```

Note that most of these are defined as optional, which is often the case for query parameters. However, we can of course require query parameters by simply defining the model field as required (like `QueryModel.x` in the above).
First, we import `validate` and `get_valid_request` from Flask-Reqcheck. Then we create a custom model using Pydantic’s `BaseClass` - in this example, it is a simple model for the expected request body. Then you annotate the Flask route function with `@validate`, providing our model of the request body. Finally, within our route function’s logic, we access the instance of the `ValidRequest` class and assign it to a variable using `vreq = get_valid_request()`. We could then call `print(vreq.body)` to obtain the instance of our request body model.

If no query model is given to the `@validate` decorator then no query parameters will be added to the valid request object. In that case they must be accessed normally via Flask's API.
More specific decorators can also be used:
- `@validate_body`
- `@validate_form`
- `@validate_path`
- `@validate_query`

### Body data
More to come.

For request bodies we must define a model for what we expect, and then pass that class into the validate decorator:
For a full example, see the [examples directory in the Flask-Reqcheck repository](/example/).

```python
class BodyModel(BaseModel):
a: str
b: int
c: float
d: uuid.UUID
arr: list[int]

@app.post("/body")
@validate(body=BodyModel)
def request_with_body():
vreq = get_valid_request()
return vreq.to_dict()
```

### Form data

Define a model for the form and then pass the class into the validate decorator:

```python
class FormModel(BaseModel):
a: str
b: int
## Contributing

@app.post("/form")
@validate(form=FormModel)
def request_with_form_data():
vreq = get_valid_request()
return vreq.to_dict()
Clone the repo, pip install locally, then make the changes. Please do the following:

```

## Contributing
- Branch off of the `develop` branch to work on changes
- Use the `/feature/{feature name}` or `/bugfix/{bugfix name}` format for branch names
- PR from your branch into `develop`
- Use the [Black](https://black.readthedocs.io/en/stable/) formatter along with [isort](https://pycqa.github.io/isort/) to keep the codebase clean. Before making a PR:
- `python -m black .`
- `python -m isort .`
- Update the docs where necessary - the `make html` command from the `/docs` directory might be enough for minor changes. Otherwise, see how they are structured and make changes accordingly. Given that the focus is on just a few functions, the rest of the API is only documented in the code itself so it might not be necessary to include that in the main docs html.
- Use `numpy` style docstrings
- Write tests - PRs will fail if the tests fail

pending...

## License

Expand Down
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
15 changes: 15 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
API
===


Decorator Functions
-------------------

.. automodule:: flask_reqcheck.decorators
:members:

Validated Request Data
----------------------

.. automodule:: flask_reqcheck.valid_request
:members:
47 changes: 47 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = "Flask-Reqcheck"
copyright = "2024, Lewis Dunne"
author = "Lewis Dunne"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
# "pallets_sphinx_themes",
"numpydoc",
]
numpydoc_show_class_members = False

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]


# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = "alabaster"
html_theme_options = {
"github_user": "lcdunne",
"github_repo": "flask-reqcheck",
"github_banner": True,
"github_button": True,
"github_type": "star",
"fixed_sidebar": True,
}

html_static_path = ["_static"]

# -- Options for todo extension ----------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration

todo_include_todos = True
37 changes: 37 additions & 0 deletions docs/flask_reqcheck.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
flask\_reqcheck package
=======================

Submodules
----------

flask\_reqcheck.decorators module
---------------------------------

.. automodule:: flask_reqcheck.decorators
:members:
:undoc-members:
:show-inheritance:

flask\_reqcheck.request\_validation module
------------------------------------------

.. automodule:: flask_reqcheck.request_validation
:members:
:undoc-members:
:show-inheritance:

flask\_reqcheck.valid\_request module
-------------------------------------

.. automodule:: flask_reqcheck.valid_request
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: flask_reqcheck
:members:
:undoc-members:
:show-inheritance:
36 changes: 36 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.. Flask-Reqcheck documentation master file, created by
sphinx-quickstart on Sat Aug 31 16:30:49 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
==============
Flask-Reqcheck
==============

**Flask-Reqcheck** lets you validate requests in your Flask applications. With a simple
decorator and some `Pydantic <https://docs.pydantic.dev/latest/>`_ models, you can quickly
validate incoming request bodies, query parameters, and url path parameters, reducing
boilerplate code and minimizing errors.

Installation
============

Install it like most others, using ``pip``::

pip install flask-reqcheck

Guide
-----
.. toctree::
:maxdepth: 2

usage


API Reference
-------------
.. toctree::
:maxdepth: 2

api

Loading

0 comments on commit 95b6322

Please sign in to comment.