Flask "url_for" equivalent for Falcon - python

I'm new to Falcon, and I was wondering if there was a Flask-like "url_for" solution for the framework. I've scoured the docs and I can't seem to find anything relevant with a google/stack search.
To clarify for users of Falcon who haven't used Flask, I would like to dynamically fetch a defined resource's URL. I'm specifically trying to achieve resource expansion, by including a link to my resources within my payload so the frontend doesn't have to construct any URLs.
Code:
class PostResource(object):
def on_get(self, req, resp, post_id):
"""Fetch single post resource."""
resp.status = falcon.HTTP_200
post_dto = post_to_dto(get_post(post_id))
# TODO: find url_to alternative for falcon: specify post resource location
post_dto.href = ''
resp.body = to_json(PostDtoSerializer, post_dto)
class PostCollectionResource(object):
def on_get(self, req, resp):
"""
Fetch grid view for all post resources.
Note: This endpoint support pagination, pagination arguments must be provided via query args.
"""
resp.status = falcon.HTTP_200
# TODO: add hrefs for each post for end ui
post_collection_dto = PostCollectionDto(
posts=[post_to_dto(post, comments=False) for post in get_posts(
start=req.params.get('start', None), count=req.params.get('count', None)
)])
resp.body = to_json(PostCollectionDtoSerializer, post_collection_dto)
def on_post(self, req, resp):
"""Create a new post resource."""
resp.status = falcon.HTTP_201
payload = req.stream.read()
user = req.context.get('user')
create_post(user._id, from_json(PostFormDtoSerializer, payload))
# TODO: find url_to alternative for falcon: redirect to on_get
resp.set_header('Location', '')
Post collection example:
[
{
"href": ".../post/000000/",
"links": [
"rel": "like",
"href": ".../post/000000/like"
],
"title": "Foobar",
...
}
]
I would like to be able to generate a link to the PostResource.

For the sake of closing this thread, I'm now using the methodology detailed here https://github.com/neetjn/py-blog/issues/16.
Falcon does not support this as confirmed by the maintainers, my work around was to create a base resource with a static route and child method to construct a link to the given resource using the information from the request's req argument.
Example:
class BaseResource(object):
route = ''
#classmethod
def url_to(cls, host, **kwargs) -> str:
return f'{host}{cls.route.format(**kwargs)}'
...
class PostResource(BaseResource):
route = '/v1/post/{post_id}'
def on_get(self, req, res):
pass
class PostCollectionResource(BaseResource):
route = '/v1/posts/'
def on_get(self, req, res):
posts = get_posts()
for post in posts:
post.href = PostResource.url_to(req.netloc, post_id=post.id)

Related

How to redirect to dynamic URL inside a FastAPI endpoint?

I'm doing a feature where the user on their profile page makes changes (not related to the user model). Everything is implemented through static HTML templates. I need the user to click on the button and return to the same page (i.e., their profile page) after processing the request.
Html template
<td>Accept</td>
endpoints.py
#router.get('/invite/{pk}/decline')
async def decline_event_invite(
request: Request,
pk: int,
user_id: str = Depends(get_current_user),
service: InviteService = Depends(),
):
await service.invite_decline(pk)
...
--> here I want redirect to user profile page
return RedirectResponse('DYNAMIC URL WITH ARGS')
profile.py
#router.get('/{pk}')
async def user_profile(
request: Request,
pk: int,
service: UserService = Depends()
):
user = await service.get_user_info(pk)
events_invites = await service.get_user_events_invite_list(pk)
return templates.TemplateResponse(
'profile.html',
context=
{
'request': request,
'user': user,
'events_invites': events_invites,
}
)
But I can't find anywhere how to do a redirect similar to the logic that applies to templates. For example:
Sender
You can use url_for() function and pass the (**kwargs) path parameters.
import uvicorn
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
import urllib
from fastapi import APIRouter
router = APIRouter()
templates = Jinja2Templates(directory="templates")
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int):
redirect_url = request.url_for('user_profile', **{ 'pk' : pk})
return RedirectResponse(redirect_url)
#router.get('/{pk}')
def user_profile(request: Request, pk: int):
return templates.TemplateResponse("profile.html", {"request": request, "pk": pk})
if __name__ == "__main__":
uvicorn.run(router, host='127.0.0.1', port=8000, debug=True)
To add query params
In case you had to pass query params as well, you could use the following code (make sure to import urllib). Alternatively, you could use the CustomURLProcessor, as described in this and this answer (which pretty much follows the same approach).
If the endpoint expected query params, for example:
#router.get('/invite/{pk}/decline')
def decline_event_invite(request: Request, pk: int, message: str):
pass
you could use:
redirect_url = request.url_for('user_profile', pk=pk)
parsed = list(urllib.parse.urlparse(redirect_url))
parsed[4] = urllib.parse.urlencode({**{ 'message' : "Success!"}})
redirect_url = urllib.parse.urlunparse(parsed)
or even use:
message = 'Success!'
redirect_url = request.url_for('user_profile', pk=pk) + f'?message={message}'
Update
Another solution would be to use Starlette's starlette.datastructures.URL, which now provides a method to include_query_params. Example:
from starlette.datastructures import URL
redirect_url = URL(request.url_for('user_profile', pk=pk)).include_query_params(message="Success!")

How do I transform a python dictionary to an HttpRequest appropriately?

I have a builtins.dict dictionary that looks like this:
request = {"data": {"var": "hello", "content": "jello"}}
I am writing a test for an API endpoint in django. I am calling the post method of that class and sending this request to it, and I am accessing the data in the request using request.data. But of course that won't work, because the request that goes into my post function is actually a django.http.HttpRequest, not a builtins.dict. How do I make it so that my request argument is a HttpRequest and not a dict so my function can consume it right?
Edit:
Okay, so I have a function that looks like this:
class EmailView(APIView):
"""
Serializes POST data for sending emails.
"""
authentication_classes = [authentication.TokenAuthentication]
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
"""
Sends an email using SES
:param request: POST request
:return: drf http response
"""
serializer = EmailSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
args = serializer.data
send_mail(args.get("subject"), args.get("message"), "info#mycomp.io", args.get("to"), fail_silently=False)
return Response('Email sent.', status=status.HTTP_200_OK)
I want to test this function. So, I've written something that looks like this:
class EmailTests(APITestCase):
def setup(self):
self.user = UserProfile.objects.create_user(
'testuser', email='testuser#test.com', password='testpass')
self.user.save()
def init_request(self):
request = {"data": {"sender": "info#mycomp.io", "to": ["test#gmail.com", "testt#gmail.com"],
"subject": "Subject", "message": "Hello"}}
return request
def test_incorrect_email(self):
request = self.init_request()
reponse = EmailView.post(EmailView, request)
print(reponse)
But, of course, this isn't working because the request i'm sending through my to my post in the test isn't an actual Request from rest_framework.request. Guess I'm saying I have no idea what the right way to write a test for this is... any ideas?
Edit:
My new tests. The EmailView class has not changed.
class EmailTests(APITestCase):
def setup(self):
self.user = UserProfile.objects.create_user(
'testuser', email='testuser#test.com', password='testpass')
self.user.save()
def init_request(self):
request = {"sender": "info#mycomp.io", "to": ["test#gmail.com", "testt#gmail.com"],
"subject": "Subject", "message": "Hello"}
return request
def test_incorrect_email(self):
request = self.init_request()
factory = APIRequestFactory()
request = factory.post('/v1/send_email/', request)
view = EmailView()
reponse = view.post(request)
print(reponse)
Use the Django REST Framework RequestFactory, eg:
from rest_framework.test import APIRequestFactory
view = EmailView.as_view()
factory = APIRequestFactory()
request = factory.post('/url/to/the/endpoint', {"var": "hello", "content": "jello"})
reponse = view(request)

Passing a header value to a get request in django

I want to pass a value through the Headers of a get request.
Im trying the below but it doesn't work,
class ListCategoriesView(generics.ListAPIView):
"""
Provides a get method handler.
"""
serializer_class = CategorySerializer
def get(self, request, *args, **kwargs):
token = request.data.get("token", "")
if not token:
"""
do some action here
"""
if not UserAccess.objects.filter(accessToken=token).exists():
"""
do some action here
"""
else:
"""
do some action here
"""
I want to pass the token in the headers like that :
can anyone help me with this issue,
thanks a lot in advance.
You said it yourself, you're passing it in the headers, so you need to get it from there. DRF does not do anything special to access the headers, so it proxies to the underlying Django HttpRequest object, which makes them available via the META attribute, converted to uppercase and prefixed by HTTP_:
token = request.META.get("HTTP_TOKEN", "")

Parse uuid from parameterized route path in falcon

I have a falcon app with a parameterized route for getting resources. The user does not know the uuid of the resource because it is temporary, so a redirect is needed.
The user will make a GET /transaction request, and a redirect to the returned path of 302 found response.
How can I parse the uuid from the request path?
The app would look like this:
api = falcon.API()
api.add_route('/transaction', Transaction)
api.add_route('/transaction/{id}', TransactionItem))
And the recources something like this:
class Transaction(object):
def on_get(self, req, resp):
id = get_current_id()
resp.status = falcon.HTTPFound('/TransactionItem/{}'.format(id))
class TransactionItem(object):
def on_get(self, req, resp):
// Parse id from path?
transaction = get_transaction(id)
// ...
// include info in the response, etc
resp.status = falcon.HTTP_200
Ok so.
Flacon passes the matched route fields as a keywords arguments. That means thats in Your TransactionItem class Your on_get must have one of the (You can chose one which is more clear for You) given definitions :
# 1st way
def on_get(self, req, resp, id=None):
...
# 2nd way (**kwargs catches all keywords args)
def on_get(self, req, resp, **kwargs):
id = kwargs.get('id')
The passed field will be dafault passed as str if You want to have it converted by falcon You can use the builtin in Falcon UUIDConverter
Here the docs for converter : https://falcon.readthedocs.io/en/stable/api/routing.html#falcon.routing.UUIDConverter

Django Rest Framework - How to test ViewSet?

I'm having trouble testing a ViewSet:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view()
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
I'm trying to replicate the syntax here:
http://www.django-rest-framework.org/api-guide/testing#forcing-authentication
But I think their AccountDetail view is different from my ViewSet, so I'm getting this error from the last line:
AttributeError: 'NoneType' object has no attributes 'items'
Is there a correct syntax here or am I mixing up concepts? My APIClient tests work, but I'm using the factory here because I would eventually like to add "request.user = some_user". Thanks in advance!
Oh and the client test works fine:
def test_client_view(self):
response = APIClient().get(reverse('cat-detail', args=(cat.pk,)))
self.assertEqual(response.status_code, 200)
I think I found the correct syntax, but not sure if it is conventional (still new to Django):
def test_view_set(self):
request = APIRequestFactory().get("")
cat_detail = CatViewSet.as_view({'get': 'retrieve'})
cat = Cat.objects.create(name="bob")
response = cat_detail(request, pk=cat.pk)
self.assertEqual(response.status_code, 200)
So now this passes and I can assign request.user, which allows me to customize the retrieve method under CatViewSet to consider the user.
I had the same issue, and was able to find a solution.
Looking at the source code, it looks like the view expects there to be an argument 'actions' that has a method items ( so, a dict ).
https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/viewsets.py#L69
This is where the error you're getting is coming from. You'll have to specify the argument actions with a dict containing the allowed actions for that viewset, and then you'll be able to test the viewset properly.
The general mapping goes:
{
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}
http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers
In your case you'll want {'get': 'retrieve'}
Like so:
class ViewSetTest(TestCase):
def test_view_set(self):
factory = APIRequestFactory()
view = CatViewSet.as_view(actions={'get': 'retrieve'}) # <-- Changed line
cat = Cat(name="bob")
cat.save()
request = factory.get(reverse('cat-detail', args=(cat.pk,)))
response = view(request)
EDIT: You'll actually need to specify the required actions. Changed code and comments to reflect this.
I found a way to do this without needing to manually create the right viewset and give it an action mapping:
from django.core.urlresolvers import reverse, resolve
...
url = reverse('cat-list')
req = factory.get(url)
view = resolve(url).func
response = view(req)
response.render()
I think it's your last line. You need to call the CatViewSet as_view(). I would go with:
response = view(request)
given that you already defined view = CatViewSet.as_view()
EDIT:
Can you show your views.py? Specifically, what kind of ViewSet did you use? I'm digging through the DRF code and it looks like you may not have any actions mapped to your ViewSet, which is triggering the error.
I needed to get this working with force authentication, and finally got it, here is what my test case looks like:
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from django.db.models.query import QuerySet
from rest_framework.test import force_authenticate
from django.contrib.auth.models import User
from config_app.models import Config
from config_app.apps import ConfigAppConfig
from config_app.views import ConfigViewSet
class ViewsTestCase(TestCase):
def setUp(self):
# Create a test instance
self.config = Config.objects.create(
ads='{"frequency": 1, "site_id": 1, "network_id": 1}',
keys={}, methods={}, sections=[], web_app='{"image": 1, "label": 1, "url": 1}',
subscriptions=[], name='test name', build='test build', version='1.0test', device='desktop',
platform='android', client_id=None)
# Create auth user for views using api request factory
self.username = 'config_tester'
self.password = 'goldenstandard'
self.user = User.objects.create_superuser(self.username, 'test#example.com', self.password)
def tearDown(self):
pass
#classmethod
def setup_class(cls):
"""setup_class() before any methods in this class"""
pass
#classmethod
def teardown_class(cls):
"""teardown_class() after any methods in this class"""
pass
def shortDescription(self):
return None
def test_view_set1(self):
"""
No auth example
"""
api_request = APIRequestFactory().get("")
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 401)
def test_view_set2(self):
"""
Auth using force_authenticate
"""
factory = APIRequestFactory()
user = User.objects.get(username=self.username)
detail_view = ConfigViewSet.as_view({'get': 'retrieve'})
# Make an authenticated request to the view...
api_request = factory.get('')
force_authenticate(api_request, user=user)
response = detail_view(api_request, pk=self.config.pk)
self.assertEqual(response.status_code, 200)
I'm using this with the django-nose test runner and it seems to be working well. Hope it helps those that have auth enabled on their viewsets.

Categories

Resources