I am trying to understand exception handling in SQLModel and SqlAlchemy.
Assume I have this code
def create_blog(blog: BlogCreate, session: Session = Depends(get_session)):
try:
result = Blog(title=blog.title, body=blog.body, author_id=blog.author_id)
session.add(result)
session.commit()
session.refresh(result)
except exc.IntegrityError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Author with {blog.author_id} not found"
)
except:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Something went wrong"
)
return result
Here, author_id is a foreign key. Now Suppose if I pass it some id for which author doesn't exist (and not doing try except), It is going to throw an error (in my terminal logs).
DETAIL: Key (author_id)=(5) is not present in table "author".
How can I get this message so that I can pass it to user?
Details of application:
UI: Angular
Backend: Python Flask (using Swagger)
Database: MongoDB
We have a few backend python methods which will be called from the UI side to do CURD operations on the database.
Each of the methods has a decorator which will check the header information to ensure that only a genuine person can call the methods.
From the UI side when these API's are called, this authorization decorator is not creating any problem and a proper response is returned to the UI (as we are passing the header information also to the request)
But now we are writing unit test cases for the API's. Here each test case will call the backend method and because of the authorization decorator, I am getting errors and not able to proceed. How can I handle this issue?
backend_api.py
--------------
from commonlib.auth import require_auth
#require_auth
def get_records(record_id):
try:
record_details = records_coll.find_one({"_id": ObjectId(str(record_id))})
if record_details is not None:
resp = jsonify({"msg": "Found Record", "data": str(record_details)})
resp.status_code = 200
return resp
else:
resp = jsonify({"msg": "Record not found"})
resp.status_code = 404
return resp
except Exception as ex:
resp = jsonify({"msg": "Exception Occured",'Exception Details': ex}))
resp.status_code = 500
return resp
commonlib/auth.py
-----------------
### some lines of code here
def require_auth(func):
"""
Decorator that can be added to a function to check for authorization
"""
def wrapper(*args, **kwargs):
print(*args,**kwargs)
username = get_username()
security_log = {
'loginId': username,
'securityProtocol': _get_auth_type(),
}
try:
if username is None:
raise SecurityException('Authorization header or cookie not found')
if not is_auth_valid():
raise SecurityException('Authorization header or cookie is invalid')
except SecurityException as ex:
log_security(result='DENIED', message=str(ex))
unauthorized(str(ex))
return func(*args, **kwargs)
return wrapper
test_backend_api.py
-------------------
class TestBackendApi(unittest.TestCase):
### some lines of code here
#mock.patch("pymongo.collection.Collection.find_one", side_effect=[projects_json])
def test_get_records(self, mock_call):
from backend_api import get_records
ret_resp = get_records('61729c18afe7a83268c6c9b8')
final_response = ret_resp.get_json()
message1 = "return response status code is not 200"
self.assertEqual(ret_resp.status_code, 200, message1)
Error snippet :
---------------
E RuntimeError: Working outside of request context.
E
E This typically means that you attempted to use functionality that needed
E an active HTTP request. Consult the documentation on testing for
E information about how to avoid this problem.
This is an example of my exception handling in a django project:
def boxinfo(request, url: str):
box = get_box(url)
try:
box.connect()
except requests.exceptions.ConnectionError as e:
context = {'error_message': 'Could not connect to your box because the host is unknown.'}
return render(request, 'box/error.html', context)
except requests.exceptions.RequestException as e:
context = {'error_message': 'Could not connect to your box because of an unknown error.'}
return render(request, 'box/error.html', context)
There is only two excepts now, but it should be more for the several request exceptions. But already the view method is bloated up by this. Is there a way to forward the except handling to a separate error method?
There is also the problem, that I need here to call the render message for each except, I would like to avoid that.
And here I also repeat for each except "could not connect to your box because", that should be set once when there appeared any exception.
I can solve it by something like this:
try:
box.connect()
except Exception as e:
return error_handling(request, e)
-
def error_handling(request, e):
if type(e).__name__ == requests.exceptions.ConnectionError.__name__:
context = {'error_message': 'Could not connect to your box because the host is unknown.'}
elif type(e).__name__ == requests.exceptions.RequestException.__name__:
context = {'error_message': 'Could not connect to your box because of an unknown error.'}
else:
context = {'error_message': 'There was an unkown error, sorry.'}
return render(request, 'box/error.html', context)
and I could of course improve the error message thing then. But overall, is it a pythonic way to handle exceptions with if/else? For example I could not catch RequestException here if ConnectionError is thrown, so I would need to catch each requests error, that looks more like an ugly fiddling...
This is a use case for decorators. If it's something more general that applies to all views (say, error logging), you can use the Django exception middleware hook, but that doesn't seem to be the case here.
With respect to the repetitive error string problem, the Pythonic way to solve it is to have a constant base string with {replaceable_parts} inserted, so that later on you can .format() them.
With this, say we have the following file decorators.py:
import functools
from django.shortcuts import render
from requests.exceptions import ConnectionError, RequestException
BASE_ERROR_MESSAGE = 'Could not connect to your box because {error_reason}'
def handle_view_exception(func):
"""Decorator for handling exceptions."""
#functools.wraps(func)
def wrapper(request, *args, **kwargs):
try:
response = func(request, *args, **kwargs)
except RequestException as e:
error_reason = 'of an unknown error.'
if isinstance(e, ConnectionError):
error_reason = 'the host is unknown.'
context = {
'error_message': BASE_ERROR_MESSAGE.format(error_reason=error_reason),
}
response = render(request, 'box/error.html', context)
return response
return wrapper
We're using the fact that ConnectionError is a subclass of RequestException in the requests library. We could also do a dictionary with the exception classes as keys, but the issue here is that this won't handle exception class inheritance, which is the kind of omission that generates subtle bugs later on. The isinstance function is a more reliable way of doing this check.
If your exception tree keeps growing, you can keep adding if statements. In case that starts to get unwieldy, I recommend looking here, but I'd say it's a code smell to have that much branching in error handling.
Then in your views:
from .decorators import handle_view_exception
#handle_view_exception
def boxinfo(request, url: str):
box = get_box(url)
box.connect()
...
That way the error handling logic is completely separate from your views, and best of all, it's reusable.
could you have something like this:
views.py
EXCEPTION_MAP = {
ConnectionError: "Could not connect to your box because the host is unknown.",
RequestException: "Could not connect to your box because of an unknown error.",
}
UNKNOWN_EXCEPTION_MESSAGE = "Failed due to an unknown error."
def boxinfo(request, url: str):
box = get_box(url)
try:
box.connect()
except (ConnectionError, RequestException) as e:
message = EXCEPTION_MAP.get(type(e)) or UNKNOWN_EXCEPTION_MESSAGE
context = {'error_message': message}
return render(request, 'box/error.html', context)
You could then just expand the EXCEPTION_MAP and the except () for any other known exception types you're expecting to catch?
if you want to reduce the duplication of "Could not connect to your box because ...
You could maybe do:
views.py
BASE_ERROR_STRING = "Could not connect to your box because {specific}"
EXCEPTION_MAP = {
ConnectionError: "the host is unknown.",
RequestException: "of an unknown error.",
}
UNKNOWN_EXCEPTION_MESSAGE = "Failed due to an unknown error."
def boxinfo(request, url: str):
box = get_box(url)
try:
box.connect()
except (ConnectionError, RequestException) as e:
specific_message = EXCEPTION_MAP.get(type(e))
if specific_message:
message = BASE_ERROR_STRING.format(specific=specific_message)
else:
message = UNKNOWN_EXCEPTION_MESSAGE
context = {'error_message': message}
return render(request, 'box/error.html', context)
I want to make use of webhook provided by Townscript to update the is_paid field in my model. The format of the data is (a json dict):- link(Under server notification API column). A piece of useful information on the link was: We will send the data in following format using post parameter data.
Here is the python code:
def payment(request):
if request.method == "POST":
posts=request.POST["data"]
result=json.dumps(posts) #converting incoming json dict to string
paymentemail(result) # emailing myself the string (working fine)
data = posts
try:
user = Profile.objects.filter(email=data['userEmailId']).first()
user.is_paid = True
user.save()
except Exception as e: paymentemail(str(e)) # emailing myself the exception
return redirect('/home')
else:
.......
The two emails correspoinding to the paymentemail() function in the above code were:
"{\"customQuestion1\":\"+9175720*****\",\"customAnswer205634\":\"+917572******\",
\"customQuestion2\":\"**** University\",\"customQuestion205636\":\"College Name\",\"ticketPrice\":**00.00,
\"discountAmount\":0.00,\"uniqueOrderId\":\"***********\",\"userName\":\"********\",
\"customQuestion205634\":\"Contact Number\",\"eventCode\":\"**********\",\"registrationTimestamp\":\"12-12-2019 22:22\",
\"userEmailId\":\"***********#gmail.com\",etc....}"
I understand that the backslashes are for escaping the quotation marks.
Second email: (which is the exception)
string indices must be integers
Does that mean that data=request.POST['data'] gives me a string thus leading to an error when I use data['userEmailId']? How do I deal with this error?
def payment(request):
if request.method == "POST":
posts=request.POST
result=json.dumps(posts) #converting incoming json dict to string
paymentemail(result) # emailing myself the string (working fine)
try:
user = Profile.objects.filter(email=posts['userEmailId']).first()
user.is_paid = True
user.save()
except Exception as e: paymentemail(str(e)) # emailing myself the exception
return redirect('/home')
else:
.......
I changed posts=request.POST["data"] since the will get only the value of key "data", instead I use posts=request.POST so that we will get the entire request as key-value pair (dict).
user = Profile.objects.filter(email=posts['userEmailId']).first()
I'm trying to create a simple flask application that takes a string from an iOS application and stores it in a local data base. I'm a bit confused whats happening in the return portion of the submitPost() function. I'm trying to return a dictionary that contains a BOOL that indicates whether the Post request executed fully. However, i'm not sure how to variate between returning a 0 or a 1.
//Function that handles the Post request
#app.route('/submitPost/', methods=['POST'])
def submitPost():
post = Post.from_json(request.json)
db.session.add(post)
db.session.commit()
return jsonify(post.to_json), {'Success': 1}
Try the below. This way if an exception is thrown when trying to insert data it will be caught and the transaction will be rolled back as well as a response being send back.
you could also replace the zero and one with True & False depending on your preference
Below code hasn't been tested
#app.route('/submitPost/', methods=['POST'])
def submitPost():
try:
post = Post.from_json(request.json)
db.session.add(post)
db.session.commit()
return jsonify({'Success': 1})
except Exception: # replace with a more meaningful exception
db.session.rollback()
return jsonify{'Failure': 0}