Query parameters in PUT call of APIClient - django

I have an API endpoint to which I want to make a PUT call which needs both a body and query parameters. I use Django's test client to call my endpoint in a test case (docs).
I read in the documentation that for a GET call, query parameters are introduced using the argument data. I also read that for a PUT call, the argument data represents the body. I miss documentation how to add query parameters in a PUT call.
In particular, this test case fails:
data = ['image_1', 'image_2']
url = reverse('images')
response = self.client.put(url,
data=data,
content_type='application/json',
params={'width': 100, 'height': 200})
And this test case passes:
data = ['image_1', 'image_2']
url = reverse('images') + '?width=100&height=200'
response = self.client.put(url,
data=data,
content_type='application/json')
In other words: is this manually URL building really necessary?

Assuming you're using rest_framework's APITestClient, I found this:
def get(self, path, data=None, secure=False, **extra):
"""Construct a GET request."""
data = {} if data is None else data
r = {
'QUERY_STRING': urlencode(data, doseq=True),
}
r.update(extra)
return self.generic('GET', path, secure=secure, **r)
whereas the put is:
def put(self, path, data='', content_type='application/octet-stream',
secure=False, **extra):
"""Construct a PUT request."""
return self.generic('PUT', path, data, content_type,
secure=secure, **extra)
and the interesting part (an excerpt from the self.generic code):
# If QUERY_STRING is absent or empty, we want to extract it from the URL.
if not r.get('QUERY_STRING'):
# WSGI requires latin-1 encoded strings. See get_path_info().
query_string = force_bytes(parsed[4]).decode('iso-8859-1')
r['QUERY_STRING'] = query_string
return self.request(**r)
so you could probably try to create that dict with QUERY_STRING and pass it to put's kwargs, I'm not sure how worthy effort-wise that is though.

I just specify what #henriquesalvaro answer more detail.
You can pass query-parameters in PUT or POST method like below.
# tests.py
def test_xxxxx(self):
url = 'xxxxx'
res = self.client.put(url,**{'QUERY_STRING': 'a=10&b=20'})
# views.py
class TestViewSet(.....):
def ...(self, request, *args, **kwargs):
print(request.query_params.get('a'))
print(request.query_params.get('b'))

Related

Data in request.body can't be found by request.data - Django Rest Framework

I'm writing a django application. I am trying to call my django rest framework from outside, and expecting an answer.
I use requests to send some data to a function in the DRF like this:
j=[i.json() for i in AttachmentType.objects.annotate(text_len=Length('terms')).filter(text_len__gt=1)]
j = json.dumps(j)
url = settings.WEBSERVICE_URL + '/api/v1/inference'
headers = {
'Content-Disposition': f'attachment; filename={file_name}',
'callback': 'http://localhost',
'type':j,
'x-api-key': settings.WEBSERVICE_API_KEY
}
data = {
'type':j
}
files = {
'file':file
}
response = requests.post(
url,
headers=headers,
files=files,
json=data,
)
In the DRF, i use the request object to get the data.
class InferenceView(APIView):
"""
From a pdf file, extract infos and return it
"""
permission_classes = [HasAPIKey]
def post(self, request):
print("REQUEST FILE",request.FILES)
print("REQUEST DATA",request.data)
callback = request.headers.get('callback', None)
# check correctness of callback
msg, ok = check_callback(callback)
if not ok: # if not ok return bad request
return build_json_response(msg, 400)
# get zip file
zip_file = request.FILES.get('file', None)
parsed = json.loads(request.data.get('type', None).replace("'","\""))
The problem is that the data in the DRF are not received correctly. Whatever I send from the requests.post is not received.
I am sending a file and a JSON together. The file somehow is received, but other data are not.
If I try to do something like
request.data.update({"type":j})
in the DRF, the JSON is correctly added to the data, so it is not a problem with the JSON I'm trying to send itself.
Another thing, request.body shows that the JSON is somehow present in the body, but request.data can't find it.
I don't want to use request.body directly because I can't understand why it is present in the body but not visible with request.data.
In this line
response = requests.post(
url,
headers=headers,
files=files,
json=data,
)
replace json=data with data=data
like this:
response = requests.post(
url,
headers=headers,
files=files,
data=data,
)

Django redirect and modify GET parameters

I am implementing magic tokens and would like clean URLs. As a consequence, I would like to remove the token from the URL upon a successful user authentication. This is my attempt:
def authenticate_via_token(get_response):
def middleware(request):
if request.session.get('authenticated', None):
pass
else:
token = request.GET.get('token', None)
if token:
mt = MagicToken.fetch_by_token(token)
if mt:
request.session['authenticated'] = mt.email
if not request.GET._mutable:
request.GET._mutable = True
request.GET['token'] = None
request.GET._mutable = False
else:
print("invalid token")
response = get_response(request)
return response
return middleware
IE, I would like to send /products/product-detail/3?token=piyMlVMrmYblRwHcgwPEee --> /products/product-detail/3
It's possible that there may be additional GET parameters and I would like to keep them. Any input would be appreciated!
This is the solution I ended up going for:
from django.urls import resolve, reverse
import urllib
def drop_get_param(request, param):
'helpful for redirecting while dropping a specific parameter'
resolution = resolve(request.path_info) #simulate resolving the request
new_params = request.GET.copy() # copy the parameters
del new_params[param] # drop the specified parameter
reversed = reverse(resolution.url_name, kwargs=resolution.kwargs) # create a base url
if new_params: #append the remaining parameters
reversed += '?' + urllib.parse.urlencode(new_params)
return reversed

Can I pass arguments to a function in monkeypatch.setattr for a function used multiple times in one view?

My web application makes API calls to Spotify. In one of my Flask views I use the same method with different endpoints. Specifically:
sh = SpotifyHelper()
...
#bp.route('/profile', methods=['GET', 'POST'])
#login_required
def profile():
...
profile = sh.get_data(header, 'profile_endpoint')
...
playlist = sh.get_data(header, 'playlist_endpoint')
...
# There are 3 more like this to different endpoints -- history, top_artists, top_tracks
...
return render_template(
'profile.html',
playlists=playlists['items'],
history=history['items'],
...
)
I do not want to make an API call during testing so I wrote a mock.json that replaces the JSON response from the API. I have done this successfully when the method is only used once per view:
class MockResponse:
#staticmethod
def profile_response():
with open(path + '/music_app/static/JSON/mock.json') as f:
response = json.load(f)
return response
#pytest.fixture
def mock_profile(monkeypatch):
def mock_json(*args, **kwargs):
return MockResponse.profile_response()
monkeypatch.setattr(sh, "get_data", mock_json)
My problem is that I need to call get_data to different endpoints with different responses. My mock.json is written:
{'playlists': {'items': [# List of playlist data]},
'history': {'items': [# List of playlist data]},
...
So for each API endpoint I need something like
playlists = mock_json['playlists']
history = mock_json['history']
I can write mock_playlists(), mock_history(), etc., but how do I write a monkeypatch for each? Is there some way to pass the endpoint argument to monkeypatch.setattr(sh, "get_data", mock_???)?
from unittest.mock import MagicMock
#other code...
mocked_response = MagicMock(side_effect=[
# write it in the order of calls you need
profile_responce_1, profile_response_2 ... profile_response_n
])
monkeypatch.setattr(sh, "get_data", mocked_response)

json serialisation of dates on flask restful

I have the following resource:
class Image(Resource):
def get(self, db_name, col_name, image_id):
col = mongo_client[db_name][col_name]
image = col.find_one({'_id':ObjectId(image_id)})
try:
image['_id'] = str(image['_id'])
except TypeError:
return {'image': 'notFound'}
return {'image':image}
linked to a certain endpoint.
However, image contains certain datetime objects inside. I could wrap this around with `json.dumps(..., default=str), but I see that there is a way of enforcing this on flask-restful. It's just not clear to me what exactly needs to be done.
In particular, I read:
It is possible to configure how the default Flask-RESTful JSON
representation will format JSON by providing a RESTFUL_JSON
attribute on the application configuration.
This setting is a dictionary with keys that
correspond to the keyword arguments of json.dumps().
class MyConfig(object):
RESTFUL_JSON = {'separators': (', ', ': '),
'indent': 2,
'cls': MyCustomEncoder}
But it's not clear to me where exactly this needs to be placed. Tried a few things and it didn't work.
EDIT:
I finally solved with this:
Right after
api = Api(app)
I added:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime):
#return int(obj.strftime('%s'))
return str(obj)
elif isinstance(obj, datetime.date):
#return int(obj.strftime('%s'))
return str(obj)
return json.JSONEncoder.default(self, obj)
def custom_json_output(data, code, headers=None):
dumped = json.dumps(data, cls=CustomEncoder)
resp = make_response(dumped, code)
resp.headers.extend(headers or {})
return resp
api = Api(app)
api.representations.update({
'application/json': custom_json_output
})
Just cleared this out, you just have to do the following:
app = Flask(__name__)
api = Api(app)
app.config['RESTFUL_JSON'] = {'cls':MyCustomEncoder}
This works both for plain Flask and Flask-RESTful.
NOTES:
1) Apparently the following part of the documentation is not that clear:
It is possible to configure how the default Flask-RESTful JSON
representation will format JSON by providing a RESTFUL_JSON attribute
on the application configuration. This setting is a dictionary with
keys that correspond to the keyword arguments of json.dumps().
class MyConfig(object):
RESTFUL_JSON = {'separators': (', ', ': '),
'indent': 2,
'cls': MyCustomEncoder}
2)Apart from the 'cls' argument you can actually overwrite any keyword argument of the json.dumps function.
Having created a Flask app, e.g. like so:
root_app = Flask(__name__)
place MyConfig in some module e.g. config.py and then configure root_app like:
root_app.config.from_object('config.MyConfig')

Django : Calling a Url with get method

I have the url and corresponding view as follows.
url(r'^(?P<token>.*?)/ack$', views.api_ACK, name='device-api_ack')
def api_ACK(request, token):
"""
Process the ACK request comming from the device
"""
logger.info('-> api_ACK', extra={'request': request, 'token' : token, 'url': request.get_full_path()})
logger.debug(request)
if request.method == 'GET':
# verify the request
action, err_msg = api_verify_request(token=token, action_code=Action.AC_ACKNOWLEDGE)
return api_send_answer(action, err_msg)
I want to call api_ACK function with request method as GET from another view api_send_answer
I am creating one url in /device/LEAB86JFOZ6R7W4F69CBIMVBYB9SFZVC/ack in api_send_answer view as follows..
def api_send_answer(action, err_msg, provisional_answer=None):
last_action = create_action(session,action=Action.AC_ACKNOWLEDGE,token=last_action.next_token,timer=500)
url = ''.join (['/device/',last_action.next_token ,'/',Action.AC_ACKNOWLEDGE])
logger.debug('Request Url')
logger.debug(url)
response = api_ACK(request=url,token=last_action.next_token) # This is wrong
Now from api_send_answer it is redirecting to api_ACK view, but how to call api_ACK with request method as GET?
Please help..Any suggestions would be helpful to me
This line
response = api_ACK(request=url,token=last_action.next_token) is wrong because view expects HttpRequest object and you give him url instead.
if you need to return view response to user, you can use redirect:
def api_send_answer(action, err_msg, provisional_answer=None):
last_action = create_action(session,action=Action.AC_ACKNOWLEDGE,token=last_action.next_token,timer=500)
url = ''.join (['/device/',last_action.next_token ,'/',Action.AC_ACKNOWLEDGE])
logger.debug('Request Url')
logger.debug(url)
return HttpResponseRedirect(url)
if you need to do something else with view response you have to use HttpRequest object not url as parameter.