Package: sanic-cookies
Sanic CookiesMuch of the code here is borrowed fromsanic_session.I wanted to make some changes that would break a big part ofsanic_session's API, so I decided to create this repo instead.Sanic cookies supports both client side and server side cookies.Main deviations from sanic_session areInterfaces are only responsible for reading/writing theSessionDict:Session management logic is handled by theSessionobjectNo race conditions:By using:asyncwithrequest['session']:request['session']['foo']='bar'instead of:request['session']['foo']='bar'It is still however possible to use thesession_dictwithout a context manager, but it will raise some warnings,
unless it's explicitly turned off (warn_lock=False)Note:The locking mechanism used here only keeps track of locks on a thread-level, which means, an application that is horizontally scaled or one that runs on more than one process won't fully benefit from the locking mechanism that sanic-cookies currently has in place and might encounter some race conditions.
I have plans to introduce a distributed locking mechanism. Probably using something like:Aioredlock.
But for now, you should know that the locking mechanism that is currently in place will not work in a multi-process environment.A simpler implementation of SessionDict that helps me sleep in peace at night. (Probably less performant)In memory interface schedules cleanup to avoid running out of memoryEncrypted client side cookie interfaceAbility to add more than one interface to the same sessionAuthenticated Session implementationSetup ⚙️$pipinstallsanic_cookiesQuick Startfromsanic_cookiesimportSession,InMemoryfromsanicimportSanicapp=Sanic()Session(app,master_interface=InMemory())@app.route('/')asyncdefhandler(request):asyncwithrequest['session']assess:sess['foo']='bar'UsageRunning multiple interfacesfromsanic_cookiesimportSession,InMemory,AioredisfromsanicimportSanicinmem=InMemory()aioredis=AioRedis(aioredis_pool_instance)app=Sanic()sess=Session(app,master_interface=inmem,session_name='my_1st_sess')sess.add_interface(aioredis)@app.route('/')asyncdefindex(request):asyncwithrequest['my_1st_session']assess:sess['foo']='bar'# At this point 'foo' = 'bar' is written both to the inmemory# interface and the aioredis interfaceasyncwithrequest['my_1st_session']assess:# When reading, your session will always read from the "master_interface"# In that case it's the inmem interfaceassertsess['foo']=='bar'# Such pattern can be useful in many cases# e.g. you want to share your session information with an analytics teamRunning multiple sessionsfromsanic_cookiesimportSession,AuthSession,InMemory,InCookieEncrypted,AioRedisfromsanicimportSanicinmem=InMemory()aioredis=Aioredis(aioredis_pool_instance)incookie=InCookieEncrypted(b'fernetsecretkey')app=Sanic()incookie_session=Session(app,master_interface=incookie,session_name='incookiesess',cookie_name='INCOOKIE')generic_session=Session(app,master_interface=inmem,session_name='session',cookie_name='SESSION')auth_session=AuthSession(app,master_interface=aioredis,session_name='auth_session',cookie_name='SECURE_SESSION')# for production (HTTPs) set `secure=True` in your auth_session,# but this will fail in local development@app.route('/')asyncdefindex(request):asyncwithrequest['incookie_session']assess:sess['foo']='bar'asyncwithrequest['session']assess:sess['bar']='baz'asyncwithrequest['auth_session']assess:sess['baz']='foo'AuthSessionFollowing up on the previous example:fromsanic_cookiesimportlogin_required@app.route('/login')asyncdeflogin(request):# 1. User verification logic# both will work (Whatever is json serializble will)# If you want to pickle an object simply change the default# encoder&decoder in the interfaces plugged in to your AuthSessionauthorized_user=123authorized_user={'user_id':123,'email':'foo@bar.baz'}# 2. Login user# Here we access the session object# (not the session dict that is accessible from the request) from the appawaitrequest.app.exts.auth_session.login_user(request,authorized_user)# 3. Use the session dict safely and exclusively for the logged in userasyncwithrequest['auth_session']assess:sess['foo']='bar'current_user=sess['current_user']assertcurrent_user==awaitrequest.app.exts.auth_session.current_user()@app.route('/logout')asyncdeflogout(request):asyncwithrequest['auth_session']assess:assertsess['foo']=='bar'# From beforeawaitrequest.app.exts.auth_session.logout_user(request)# Resets the sessionasyncwithrequest['auth_session']assess:assertsess.get('foo')isNone# should never failassertsess.get('current_user')isNone# should never fail@app.route('/protected')@login_required()asyncdefprotected(request):assertawaitrequest.app.exts.auth_session.current_user()isnotNone# should never failInterfaces availableIn memoryfromsanic_cookiesimportSession,InMemoryfromsanicimportSanicinterface=InMemory()app=Sanic()Session(app,master_interface=interface)# You can skip this part if you don't want scheduled interface cleanup@app.listener('before_server_start')definit_inmemory(app,loop):interface.init()@app.listener('after_server_stop')defkill_inmemory(app,loop):interface.kill()@app.route('/')asyncdefhandler(request):asyncwithrequest['session']assess:sess['foo']='bar'AioredisfromaioredisimportAioredisfromsanic_cookiesimportAioredisasAioredisInterfacefromsanicimportSanicapp=Sanic()aioredis_pool_instance=Aioredis()aioredis=AioredisInterface(aioredis_pool_instance)Session(app,master_interface=interface)@app.route('/')asyncdefhandler(request):asyncwithrequest['session']assess:sess['foo']='bar'Encrypted in-cookie (using the amazing cryptography.Fernet library)i. Open a Python terminal and generate a new Fernet key:>>>fromcryptography.fernetimportFernet>>>SESSION_KEY=Fernet.generate_key()>>>print(SESSION_KEY)b'copy me to your sanic app and keep me really secure'ii. Write your appfromsanicimportSanicfromsanic_cookiesimportSession,InCookieEncryptedapp=Sanic()app.config.SESSION_KEY=SESSION_KEYSession(app,master_interface=InCookieEncrypted(app.config.SESSION_KEY),)@app.route('/')asyncdefhandler(request):asyncwithrequest['session']assess:sess['foo']='bar'Gino-AsyncPG (Postgres 9.5+):i. Manually create a table:CREATETABLEIFNOTEXISTSsessions(created_attimestampwithouttimezoneNOTNULL,expires_attimestampwithouttimezone,sidcharactervarying,valcharactervarying,CONSTRAINTsessions_pkeyPRIMARYKEY(sid));ii. Add the interface:fromsanicimportSanicfromgino.ext.sanicimportGinofromsanic_cookiesimportGinoAsyncPGfromsomething_secureimportDB_SETTINGSapp=Sanic()app.config.update(DB_SETTINGS)db=Gino()db.init_app(app)interface=GinoAsyncPG(client=db)auth_session=AuthSession(app,master_interface=interface)if__name__=='__main__':app.run(host='127.0.0.1',port='8080')Sessions availableSession (A generic session interface)AuthSession (A session interface with login_user, logout_user, current_user logic)Other pluggable partsEncoders and Decoders (Default to ujson)SID factory (Default to uuid.uuid4)Session dict implementationContact 📧I currently work as a freelance software devloper. Like my work and got a gig for me?Want to hire me fulltime? Send me an email @omarryhan@gmail.comBuy me a coffee ☕Bitcoin:3NmywNKr1Lzo8gyNXFUnzvboziACpEa31zEthereum:0x1E1400C31Cd813685FE0f6D29E0F91c1Da4675aEBitcoin Cash:qqzn7rsav6hr3zqcp4829s48hvsvjat4zq7j42wkxdLitecoin:MB5M3cE3jE4E8NwGCWoFjLvGqjDqPyyEJpPaypal:https://paypal.me/omarryhan