Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exp #5: Try FastDepends lib #11

Merged
merged 1 commit into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"extensions":[
"ms-python.python",
"njpwerner.autodocstring",
"rangav.vscode-thunder-client"
"rangav.vscode-thunder-client",
"charliermarsh.ruff"
]
}
}
Expand Down
7 changes: 6 additions & 1 deletion flask-dependency-injection-poc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ Experiment with alternative approaches to inject dependencies in a Flask applica



## Journal log
## Experimentation log
#### `@2024-11-19`: Experiment #3: Try FastDepends lib
Made good progress trying to inject depedencies, as well as validating requests with Pydantic.
Currently facing problems with dependency overriding.
See [app_3_fastdepends.py](./app_3_fastdepends.py).

#### `2024-11-19` Challenge #2: Flask-Pydantic conflict with Flask-Injector
Problem: Flask-Pydantic tries to validate the type of injected dependecy.
...
Expand Down
157 changes: 157 additions & 0 deletions flask-dependency-injection-poc/app_3_fastdepends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
from functools import wraps
from typing import Annotated, Callable
from flask import Blueprint, Flask
from pydantic import BaseModel
from fast_depends import inject, Depends
from fast_depends.library import CustomField

# schemas


class WebhookRequest(BaseModel):
payload: dict


# services
class WebhookService:
def process_webhook(self, payload):
print("Processing webhook:", payload)


# decorator that injects user_id into the view
def include_user_id():
def decorator(f: Callable) -> Callable:
"""
When you decorate a function with @wraps(original_function), it ensures that:
1. The wrapped function (wrapper) retains the metadata (such as the name, docstring, and attributes) of the original function (f).
2. The wrapped function looks like the original function in tools and frameworks that inspect function metadata, such as Flask’s app.view_functions.
"""

@wraps(f)
def wrapper(*args, **kwargs):
user_id = "123"
return f(user_id, *args, **kwargs)

return wrapper

return decorator


# blueprints
webhooks_api = Blueprint("webhooks_api", __name__)


def get_webhook_service():
return WebhookService()


# ✅ Experiment #1: Ability to inject the service into the view
@webhooks_api.route("/exp1", methods=["GET"])
@inject
def my_webhook(webhook_service=Depends(get_webhook_service)):
print(webhook_service.process_webhook({"foo": "bar"}))
return "Webhook processed successfully"


class Validate(CustomField):
def get_body_dict(self):
# inspired on flask_pydantic: https://github.com/bauerji/flask-pydantic/blob/8595fa8b5513a336c9c679829f49ddc20f56377d/flask_pydantic/core.py#L87
from flask import request

data = request.get_json()
if data is None:
return {}
return data

def use(self, /, **kwargs):
if self.param_name == "body":
kwargs = super().use(**kwargs)
kwargs["body"] = self.get_body_dict()
return kwargs


# ✅ Experiment #1.2: Request validation using Pydantic. Based on: https://lancetnik.github.io/FastDepends/#usage
# 💥 Challenge A: `@inject` (see source code) can't see the request body because Flask doesn't pass it to the view (see `flask/app.py/Flask.dispatch_request` source code)
# and (of course), the library doesn't have Flask as a dependency.
# ✅ Solved: we need to use a custom field that will extract the request body from Flask.request,
# implemented through the `Validate` class
@webhooks_api.route("/exp1.2", methods=["POST"])
@inject
def request_validation(body: WebhookRequest = Validate()):
"""
@inject decorator plays multiple roles at the same time:
- resolve Depends classes
- cast types according to Python annotation
- validate incoming parameters using pydantic
"""
print(body)
return "Webhook processed successfully"


# ✅ Experiment #2: DI injection + request validation
# Discovery A: can use type aliasing to make the code more readable, less verbose, and more maintainable!
# Eg: WebhooksService = Annotated[WebhookService, Depends(get_webhook_service)]
WebhooksService = Annotated[WebhookService, Depends(get_webhook_service)]


@webhooks_api.route("/exp2", methods=["POST"])
@inject
def my_webhook2(
# webhooks_service: WebhooksService, # 💥 see Challenge #3/A
webhooks_service: WebhookService = Depends(get_webhook_service),
body: WebhookRequest = Validate(),
):
webhooks_service.process_webhook(body.payload)
return "Webhook processed successfully"


# Experiment #3: Dependencies Overriding (Testing) - see test_app.py
# 💥 Challenge A: override seems to work with Exp#1, which uses `Depends(get_webhook_service)`, but not
# with alias: `WebhooksService = Annotated[WebhookService, Depends(get_webhook_service)]`, nor `webhooks_service: WebhookService = Depends(get_webhook_service)`
#
# Error: pydantic_core._pydantic_core.ValidationError: 1 validation error for my_webhook2
# webhooks_service
# Input should be an instance of WebhookService [type=is_instance_of, input_value=<test_app_3_fastdepends.g...bject at 0xffff9bc4eaa0>, input_type=get_fake_webhook_service.<locals>.FakeWebhookService]
# For further information visit https://errors.pydantic.dev/2.9/v/is_instance_of
# (The same issue doesn't happen on FastAPI)
#
# @2024-11-24: Opened issue: https://github.com/Lancetnik/FastDepends/issues/150
#

def create_app():
app = Flask(__name__)

@app.route("/")
def hello_world():
return """
<html>
<body>
<form id="myForm">
<input type="submit" value="Submit to /exp2">
</form>
<script>
document.getElementById('myForm').addEventListener('submit', function(event) {
event.preventDefault();
fetch('/exp2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"payload": {"foo": "bar"}})
}).then(response => response.json()).then(data => {
console.log(data);
});
});
</script>
</body>
</html>
"""

app.register_blueprint(webhooks_api)

return app


if __name__ == "__main__":
app = create_app()
app.run(debug=True)
Loading