I create a flask application for my web server and one of the endpoint is to update contact information. One contact has many attributes such as username, email, first_name, last_name etc. I declare below method to handle update request on the contact:
def update_contact_with_form(contact_id, id=None, username=None, first_name=None, last_name=None, email=None, password=None, phone=None, user_status=None):
session = Session()
try:
contact = session.query(DBContact).filter(DBContact.id == contact_id).one()
if username != None:
contact.username = username
if first_name != None:
contact.first_name = first_name
...
except Exception as error:
print(error)
finally:
session.close()
return abort(400, 'failed to update')
The above code works fine but what I don't like is to check each value against None. Is there a better way to update the instance without checking each attribute?
How about this way?
def update_contact_with_form(contact_id, **kwargs):
session = Session()
try:
contact = session.query(DBContact).filter(DBContact.id == contact_id).one()
for k, v in kwargs:
if v is not None:
contact.__setattr__(k, v)
except Exception as error:
print(error)
finally:
session.close()
return abort(400, 'failed to update')
Related
I am trying to restrict logged user to access URL routes that are not assigned to them. As soon a user had logged, for example user1, will be redirected to https://myurl.com/user1. So far, that works good, but I would like to avoid that user1 can see the content in the route of user2 in https://myurl.com/user2.
Below you can see the code I am currently using.
import tornado
from tornado.web import RequestHandler
import sqlite3
# could define get_user_async instead
def get_user(request_handler):
return request_handler.get_cookie("user")
# could also define get_login_url function (but must give up LoginHandler)
login_url = "/login"
db_file = "user_login.db"
connection = None
cursor = None
# optional login page for login_url
class LoginHandler(RequestHandler):
def get(self):
try:
errormessage = self.get_argument("error")
except Exception:
errormessage = ""
self.render("login.html", errormessage=errormessage)
def check_permission(self, username, password):
connection = sqlite3.connect(db_file)
cursor = connection.cursor()
cursor.execute("SELECT * FROM users WHERE username=? AND password=?", (username, password))
data = cursor.fetchone()
if username == data[1] and password == data[2]:
return True
return False
def post(self):
username = self.get_argument("username", "")
password = self.get_argument("password", "")
auth = self.check_permission(username, password)
if auth:
self.set_current_user(username)
self.redirect(self.get_argument("next", f"/{username}"))
else:
error_msg = "?error=" + tornado.escape.url_escape(
"Login failed, please try again or contact your system administrator."
)
self.redirect(login_url + error_msg)
def set_current_user(self, user):
if user:
self.set_cookie("user", tornado.escape.json_encode(user))
else:
self.clear_cookie("user")
# optional logout_url, available as curdoc().session_context.logout_url
# logout_url = "/logout"
# optional logout handler for logout_url
class LogoutHandler(RequestHandler):
def get(self):
self.clear_cookie("user")
self.redirect(self.get_argument("next", "/"))
The scenario is the following:
I have a database where I store users and I want to keep the data (database) and the logic (bussiness) layers completely isolated.
I've created several specific exceptions to display the correct message in the interface (Flask) layer. Such as UserNotFound, UserEmailUsed, UserNameTaken
All the code is in a single file cruduser.py
Conection to the database:
def connect():
'''Connect to the database'''
return MySQLdb.connect(
"localhost",
"root",
"****",
"myflaskapp",
cursorclass=MySQLdb.cursors.DictCursor
)
Get users by email and/or username (Returns a User object):
def get_user_by_email(user):
'''Receive a user object, check whether email is used, if not raise
UserNotFound else creates a user object and returns'''
with connect() as cursor:
cursor.execute(
'''SELECT * FROM users WHERE email = %s''',
[user.email]
)
user = cursor.fetchone()
if user is None:
raise UserNotFound
return User(**user)
def get_user_by_username(user):
'''Receive a user object, check whether username is used, if not raise
UserNotFound else creates a user object and returns'''
with connect() as cursor:
cursor.execute(
'''SELECT * FROM users WHERE username = %s''',
[user.username]
)
user = cursor.fetchone()
if user is None:
raise UserNotFound
return User(**user)
Now to check duplicates I created the following code when a new user signs up:
def create_user(user):
'''Receive a user object, check if it is available and creates a new user
and inserts it into the database'''
available_user(user)
with connect() as cursor:
cursor.execute(
"""INSERT INTO users(email, username, password) VALUES(%s,%s,%s)""",
(user.email, user.username, user.password)
)
def available_user(user):
'''Receive a user object, check email and username, if email is used, raise
UserEmailUsed if username is used raise UserNameTaken else returns True'''
try:
get_user_by_email(user)
raise UserEmailUsed
except UserNotFound:
pass
try:
get_user_by_username(user)
raise UserNameTaken
except UserNotFound:
pass
return True
My questions are:
Is this approach pythonic?
Is there a better way to write the available_user function?
Is it appropriate to use exceptions in this way?
Note: This is a learning project so I would like to keep as pure as possible, no flask plugins at this point.
I am not too experienced with custom Exceptions but it looks like both of them are raised in any case, in contrast to your comment (if). So I'd put this condition into the code, and maybe catch UserNotFound in a common except.
try:
if get_user_by_email(user) :
raise UserEmailUsed
if get_user_by_username(user) :
raise UserNameTaken
except UserNotFound:
pass # handled elsewhere?
https://docs.python.org/3/tutorial/errors.html#handling-exceptions
After think and try several solutions I came to this:
First, DB logic and Business Logic should be separated so there is a dbconnect.py file with the following code:
def connect():
'''Connect to the database'''
return psycopg2.pool.SimpleConnectionPool(
1,
30,
host='ec2-23-23-223-2.compute-1.amazonaws.com',
user='wmowxsiematqzx',
password='d46dff42de4d8af492900f33d322caa13a0e05174b2310eec2e3549295438ab1',
dbname='dfciqalcgr98ah'
)
#contextmanager
def get_cursor():
a = connect()
con = a.getconn()
con.autocommit = True
try:
yield con.cursor()
finally:
a.putconn(con)
def execute_query_void(query):
with get_cursor() as cursor:
cursor.execute(*query)
def execute_query_one(query):
with get_cursor() as cursor:
cursor.execute(*query)
result = cursor.fetchone()
return result
def execute_query_many(query):
with get_cursor() as cursor:
cursor.execute(*query)
result = cursor.fetchall()
return result
All possible scenarios are covered there.
Now the cruduser.py
def get_user_by_email(email):
'''Receive a user object, look for a user with the given email and
returns a user object or None'''
query = ('''SELECT email, username, password_hash, rolename FROM users
INNER JOIN roles ON users.idroles=roles.idroles
WHERE email = %s''',
[email])
user = execute_query_one(query)
if user is None:
return None
return User(**user)
def get_user_by_username(user):
'''Receive a user object, look for a user with the given username and
returns a user object or None'''
query = ('''SELECT email, username, password_hash, rolename FROM users
INNER JOIN roles ON users.idroles=roles.idroles
WHERE username = %s''',
[user.username])
user = execute_query_one(query)
if user is None:
return None
return User(**user)
And the available functionality is achieved with two functions:
def available_user(user):
'''Receive a user object, check email and username, if email is used, raise
UserEmailUsed if username is used raise UserNameTaken else returns True'''
users = get_users(user)
if len(users) == 0:
return True
for user_ in users:
if user.email == user_.email:
raise UserEmailUsed
if user.username == user_.username:
raise UserNameTaken
def get_users(user):
'''Receive a article object, check whether articlename is used, if not raise
articleNotFound else creates a article object and returns'''
query = ('''SELECT email, username
FROM users
WHERE username=%s OR email=%s;''',
(user.username, user.email))
users = execute_query_many(query)
return [User(**user) for user in users]
The reason why I switch between using get_user_by_email and get_user_by_username to get_users is for performance, the latter just uses two columns while the former returns a whole object after performing a Join.
Additionally the exception where renamed to a more explicit name and where moved from DB Logic to Business Logic file
I am trying to create a form to edit a user account.
This code is for a user that already exists.
As you can see, I have put in a return statement before the try/catch.
This will generate the expected integrity exception when a username already exists.
However if I comment that out and let it go into the try/catch block, no exception is generated, it simply redirects to the HTTPFound with no error.
the call to flush does nothing.
if form.validate():
user.username = form.username.data
if form.password.data != "":
user.password = encrypt(form.password.data)
user.is_active = form.is_active.data
user.email = form.email.data
user.groups = form.groups.data
# if i return here i will get integrity error as expected
#return HTTPFound(location=request.route_url('user_details', id=user.id))
with transaction.manager as tx:
try:
request.sqldb.flush()
# no exception is generated here
return HTTPFound(location=request.route_url('user_details', id=user.id))
except IntegrityError as e:
tx.abort()
if 'UNIQUE constraint failed: users.username' in str(e):
form.username.errors.append('Username already exists')
else:
errors.append('Integrity Error')
edit
here is the code that works
the transaction manager part was not even required:
user.username = form.username.data
if form.password.data != "":
user.password = encrypt(form.password.data)
user.is_active = form.is_active.data
user.email = form.email.data
user.groups = form.groups.data
try:
request.sqldb.flush()
return HTTPFound(location=request.route_url('user_details', id=user.id))
except IntegrityError as e:
if '(IntegrityError) duplicate key value' in str(e):
errors.append('Username already exists')
else:
errors.append('Integrity Error')
request.sqldb.rollback()
You need to fetch the model, and do changes to the model objects within the transaction manager, otherwise the session's flush() has a limited scope. The SQLAlchemy is smart enough to try to flush only that subset of changes that occur within the context manager. Thus:
with transaction.manager as tx:
try:
user = .... # get the user
user.username = form.username.data
if form.password.data != "":
user.password = encrypt(form.password.data)
user.is_active = form.is_active.data
user.email = form.email.data
user.groups = form.groups.data
request.sqldb.flush()
# no exception is generated here
return HTTPFound(location=request.route_url('user_details', id=user.id))
except IntegrityError as e:
tx.abort()
if 'UNIQUE constraint failed: users.username' in str(e):
form.username.errors.append('Username already exists')
else:
errors.append('Integrity Error')
My solution is based on global exception handling.
http://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/pylons/exceptions.html
from pyramid.view as view
import sqlalchemy.exc
#view.view_config(context=sqlalchemy.exc.IntegrityError, renderer='json')
def integrity_error(exc, request):
request.response.status_code = 400
return {
'error': exc.orig.args[1]
}
or
#view.exception_view_config(sqlalchemy.exc.IntegrityError, renderer='json')
def integrity_error(exc, request):
request.response.status_code = 400
return {
'error': exc.orig.args[1]
}
The differernce is described at http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/views.html
In this way, I can get a RESTful Api from Pyramid. It is rough, but useful in fast prototyping, and can save lots of code in routes.
I am setting up a Flask-RESTful service and have user authentication working. The method that I'm using is:
def generate_auth_token(username, expiration=600):
gen_serial = Serializer(secret_key, expires_in=expiration)
return gen_serial.dumps({'username': username})
I pass the token to the user as follows:
class token(Resource):
decorators = [auth.login_required]
def post(self):
username = g.user
return_token = generate_auth_token(username)
return {'token':return_token.decode()}, 200
And the token is then verified as such so that it does not need to be stored server side:
def verify_auth_token(auth_token):
serial = Serializer(secret_key)
try:
data = serial.loads(auth_token)
except SignatureExpired:
return None
except BadSignature:
return None
serial_user = data['username']
return serial_user
This seems to work well, however I am unsure how to logout the user before the expiration is expired without storing the token serverside. My thought was to pass back a garbage token when the user elects to logout, but I don't think this is an elegant or secure solution.
Any tips would be really helpful!
Rather than a garbage token simply encode no data:
def generate_auth_token(username=None, expiration=600):
gen_serial = Serializer(secret_key, expires_in=expiration)
data = {'username': username} if username is not None else {}
return gen_serial.dumps(data)
Then you can have an invalidate endpoint that requires a login and returns a token without a username:
def invalidate(self):
return_token = generate_auth_token()
return {'token':return_token.decode()}, 200
At that point, you can just handle the possibly missing username field:
def verify_auth_token(auth_token):
serial = Serializer(secret_key)
try:
data = serial.loads(auth_token)
except SignatureExpired:
return None
except BadSignature:
return None
serial_user = data.get('username')
return serial_user
Is there a better pattern for input validation than I'm using in this function?
https://github.com/nathancahill/clearbit-intercom/blob/133e4df0cfd1a146cedb3c749fc1b4fac85a6e1b/server.py#L71
Here's the same function without any validation. It's much more readable, it's short and to the point (9 LoC vs 53 LoC).
def webhook(clearbitkey, appid, intercomkey):
event = request.get_json()
id = event['data']['item']['id']
email = event['data']['item']['email']
person = requests.get(CLEARBIT_USER_ENDPOINT.format(email=email), auth=(clearbitkey, '')).json()
domain = person['employment']['domain']
company = requests.get(CLEARBIT_COMPANY_ENDPOINT.format(domain=domain), auth=(clearbitkey, '')).json()
note = create_note(person, company)
res = requests.post(INTERCOM_ENDPOINT,
json=dict(user=dict(id=id), body=note),
headers=dict(accept='application/json'),
auth=(appid, intercomkey))
return jsonify(note=res.json())
However, it doesn't handle any of these errors:
dict KeyError's (especially nested dicts)
HTTP errors
Invalid JSON
Unexpected responses
Is there a better pattern to follow? I looked into using a data validation library like voluptous but it seems like I'd still have the same problem of verbosity.
Your original code on github seems fine to me. It's a little over complicated, but also handle all cases of error. You can try to improve readability by abstract things.
Just for demonstration, I may write code like this:
class ValidationError(Exception):
"Raises when data validation fails"
pass
class CallExternalApiError(Exception):
"Raises when calling external api fails"
pass
def get_user_from_event(event):
"""Get user profile from event
:param dict event: request.get_json() result
:returns: A dict of user profile
"""
try:
event_type = event['data']['item']['type']
except KeyError:
raise ValidationError('Unexpected JSON format.')
if event_type != 'user':
return ValidationError('Event type is not supported.')
try:
id = event['data']['item']['id']
email = event['data']['item']['email']
except KeyError:
return ValidationError('User object missing fields.')
return {'id': id, 'email': email}
def call_json_api(request_function, api_name, *args, **kwargs):
"""An simple wrapper for sending request
:param request_function: function used for sending request
:param str api_name: name for this api call
"""
try:
res = request_function(*args, **kwargs)
except:
raise CallExternalApiError('API call failed to %s.' % api_name)
try:
return res.json()
except:
raise CallExternalApiError('Invalid response from %s.' % api_name)
#app.route('/<clearbitkey>+<appid>:<intercomkey>', methods=['POST'])
def webhook(clearbitkey, appid, intercomkey):
"""
Webhook endpoint for Intercom.io events. Uses this format for Clearbit and
Intercom.io keys:
/<clearbitkey>+<appid>:<intercomkey>
:clearbitkey: Clearbit API key.
:appid: Intercom.io app id.
:intercomkey: Intercom.io API key.
Supports User events, specifically designed for the User Created event.
Adds a note to the user with their employment and company metrics.
"""
event = request.get_json()
try:
return handle_event(event, clearbitkey, appid, intercomkey)
except (ValidationError, CallExternalApiError) as e:
# TODO: include **res_objs in response
return jsonify(error=str(e))
def handle_event(event):
"""Handle the incoming event
"""
user = get_user_from_event(event)
res_objs = dict(event=event)
person = call_json_api(
requests.get,
'Clearbit',
CLEARBIT_USER_ENDPOINT.format(email=user['email']),
auth=(clearbitkey, '')
)
res_objs['person'] = person
if 'error' in person:
raise CallExternalApiError('Error response from Clearbit.')
domain = person['employment']['domain']
company = None
if domain:
try:
company = call_json_api(
requests.get,
'Clearbit',
CLEARBIT_COMPANY_ENDPOINT.format(domain=domain),
auth=(clearbitkey, ''))
)
if 'error' in company:
company = None
except:
company = None
res_objs['company'] = company
try:
note = create_note(person, company)
except:
return jsonify(error='Failed to generate note for user.', **res_objs)
result = call_json_api(
requests.post,
'Intercom',
(INTERCOM_ENDPOINT, json=dict(user=dict(id=id), body=note),
headers=dict(accept='application/json'),
auth=(appid, intercomkey)
)
return jsonify(note=result, **res_objs)
I hope it helps.