From e6e481d0ebe4a59e80a4a6de59921767e421464f Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 09:43:00 +1000 Subject: [PATCH 01/12] initial commit --- example_redis.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 example_redis.py diff --git a/example_redis.py b/example_redis.py new file mode 100644 index 0000000..597cdb6 --- /dev/null +++ b/example_redis.py @@ -0,0 +1,55 @@ +""" + example_redis.py + + test redis middleware +""" + +from lona import App, View +from lona.html import H1, HTML, P + +from loguru import logger + + +app = App(__file__) + +app.settings.SESSIONS = True +app.settings.MIDDLEWARES = [ + "lona_redis.middlewares.RedisSessionMiddleware", +] + +app.settings.REDIS_USER = "some_redis_user" +app.settings.REDIS_PASSWORD = "abcd1234" + + +@app.route("/") +class Index(View): + def handle_request(self, request): + # request.user.session.set(foo=123) + # OR + # foo = 123 + # request.user.session.set(foo=foo) + # foo = request.user.session.get("foo") + # logger.debug(f"{foo=}") + # logger.debug(f"{request.user.session._actual_redis_key('foo')=}") + + list_var = [1, 2.222, "string"] + int_var = 123 + float_var = 123.456 + tuple_var = (1, 2, 3) + dict_var = {"a": 1, "b": 2} + + # NOTE example of using Redis commands directly + # https://redis.readthedocs.io/en/latest/commands.html#core-commands + request.user.session.r.set("myKey", "thevalueofmykey") + myKey = request.user.session.r.get("myKey") + all_keys = request.user.session.r.keys() + logger.debug(f"{all_keys=}") + + # return HTML(H1(request.user.session["count"])) + return HTML( + H1("Hello World"), + # P("Lorem Ipsum"), + ) + + +app.run() From 8d23e18917f8e2e6eaf9e6d2ae126f2719721d38 Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 09:43:20 +1000 Subject: [PATCH 02/12] wip --- lona_redis/middlewares.py | 108 +++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index ffa1e8f..df789f3 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -1,26 +1,118 @@ +import pickle + +import redis +from loguru import logger + + class RedisSession: - def __init__(self, redis_user): - self.redis_user = redis_user + # def __init__(self, redis_user): + # self.redis_user = redis_user - def get(self, *args, **kwargs): - raise NotImplementedError() + def __init__(self, *args, **kwargs): + """ + initalize redis.Redis() + + https://redis.readthedocs.io/en/latest/#quickly-connecting-to-redis + https://redis.readthedocs.io/en/latest/examples/connection_examples.html + + there are many many kwargs: + https://redis.readthedocs.io/en/latest/connections.html + """ + + # logger.debug("init RedisSession") + # FIXME pass kwargs straight through to redis.Redis? + self.r = redis.Redis(**kwargs) + self.user_request_session_key = None + + def _actual_redis_key(self, user_key): + """ + combine self.user_request_session_key with user's key + so that the ACTUAL key used to store value in Redis is unique + """ + user_request_session_key = self.user_request_session_key + return user_request_session_key + ":" + user_key def set(self, *args, **kwargs): - raise NotImplementedError() + """ + let user easily set values + eg. request.user.session.set(foo=123) + """ + # logger.debug(f"set RedisSession {args=} {kwargs=}") + for user_key, value in kwargs.items(): + actual_redis_key = self._actual_redis_key(user_key) + # logger.debug(f"{user_key=} {actual_redis_key=} {value=}") + # self.r.set(actual_redis_key, value) + self.r.set(actual_redis_key, pickle.dumps(value)) + + # def get(self, *args, **kwargs): + # raise NotImplementedError() + + def get(self, *args): + """ + let user easily get values + eg. request.user.session.get("foo") + """ + user_key = args[0] + actual_redis_key = self._actual_redis_key(user_key) + # logger.debug(f"{user_key=} {actual_redis_key=}") + + # return self.r.get(actual_redis_key) + return pickle.loads(self.r.get(actual_redis_key)) class RedisUser: def __init__(self, connection): - self.connection = connection - self.session = RedisSession(self) + # self.connection = connection + # self.session = RedisSession(self) + pass def __eq__(self, other): raise NotImplementedError() class RedisSessionMiddleware: + async def on_startup(self, data): + """ + initalize Redis connection + """ + + settings = data.server.settings + + # FIXME FIXME FIXME + # TODO what should the settings be? + # TODO there are many kwargs (https://redis.readthedocs.io/en/latest/connections.html) + # TODO maybe settings should be a dict to override the defaults? + + # logger.debug(f"{settings.REDIS_USER=}") + # logger.debug(f"{settings.REDIS_PASSWORD=}") + + # default args + # self.redis_session = RedisSession(host="localhost", port=6379, db=0) + + # FIXME pass kwargs (see note above) + self.redis_session = RedisSession() + + # to be set in handle_request() just prior to allowing user to call get(), set() + self.user_request_session_key = None + + return data + def handle_connection(self, data): - connection.user = RedisUser(data.connection) + # connection.user = RedisUser(data.connection) + # logger.debug(f"{dir(data.connection)=}") return data + def handle_request(self, data): + request = data.request + + # set self.user_request_session_key to request.user.session_key + # so that subsequent calls by the user to + # self.redis_session.set, self.redis_session.get + # ie. in app->View->handle_request() : request.user.session.set(), request.user.session.get() + # will have request.user.session_key available + # we NEED this so that each Redis key is unique to request.user.session_key + self.redis_session.user_request_session_key = request.user.session_key + request.user.session = self.redis_session + + return data From bb704f0906178566dc4213f0622296db57ece92b Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 10:10:09 +1000 Subject: [PATCH 03/12] better examples --- example_redis.py | 49 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/example_redis.py b/example_redis.py index 597cdb6..c70e764 100644 --- a/example_redis.py +++ b/example_redis.py @@ -24,6 +24,10 @@ @app.route("/") class Index(View): def handle_request(self, request): + # + # NOTE examples of using request.user.session.set, request.user.session.get + # + # request.user.session.set(foo=123) # OR # foo = 123 @@ -32,13 +36,44 @@ def handle_request(self, request): # logger.debug(f"{foo=}") # logger.debug(f"{request.user.session._actual_redis_key('foo')=}") - list_var = [1, 2.222, "string"] - int_var = 123 - float_var = 123.456 - tuple_var = (1, 2, 3) - dict_var = {"a": 1, "b": 2} + request.user.session.set(str_var="hello world") + request.user.session.set(list_var=[1, 2.222, "hello world"]) + request.user.session.set(int_var=123) + request.user.session.set(float_var=123.456) + request.user.session.set(tuple_var=(1, 2, 3)) + request.user.session.set(dict_var={"a": 1, "b": 2}) + request.user.session.set(boolean_var=True) + request.user.session.set( + mixed_var_1=[ + True, + {"a": 1, "b": 2}, + (1, 2, 3), + 123.456, + 123, + [1, 2.222, "hello world"], + "hello world", + ] + ) + request.user.session.set( + mixed_var_2={ + "a": [1, 2, 3], + "b": {"a": 1, "b": 2}, + } + ) + + logger.debug(f"{request.user.session.get('str_var')=}") + logger.debug(f"{request.user.session.get('list_var')=}") + logger.debug(f"{request.user.session.get('int_var')=}") + logger.debug(f"{request.user.session.get('float_var')=}") + logger.debug(f"{request.user.session.get('tuple_var')=}") + logger.debug(f"{request.user.session.get('dict_var')=}") + logger.debug(f"{request.user.session.get('boolean_var')=}") + logger.debug(f"{request.user.session.get('mixed_var_1')=}") + logger.debug(f"{request.user.session.get('mixed_var_2')=}") - # NOTE example of using Redis commands directly + # + # NOTE examples of using Redis commands directly + # # https://redis.readthedocs.io/en/latest/commands.html#core-commands request.user.session.r.set("myKey", "thevalueofmykey") myKey = request.user.session.r.get("myKey") @@ -48,7 +83,7 @@ def handle_request(self, request): # return HTML(H1(request.user.session["count"])) return HTML( H1("Hello World"), - # P("Lorem Ipsum"), + # P("Lorem Ipsum"), ) From 365cc6f8d0f5da858a58a0d7f1157d1536c16293 Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 10:11:07 +1000 Subject: [PATCH 04/12] cleaned up debug, comments --- lona_redis/middlewares.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index df789f3..642fb9a 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -5,8 +5,9 @@ class RedisSession: + # FIXME this isn't used - can be removed? # def __init__(self, redis_user): - # self.redis_user = redis_user + # self.redis_user = redis_user def __init__(self, *args, **kwargs): """ @@ -19,29 +20,27 @@ def __init__(self, *args, **kwargs): https://redis.readthedocs.io/en/latest/connections.html """ - # logger.debug("init RedisSession") # FIXME pass kwargs straight through to redis.Redis? self.r = redis.Redis(**kwargs) - self.user_request_session_key = None def _actual_redis_key(self, user_key): """ combine self.user_request_session_key with user's key so that the ACTUAL key used to store value in Redis is unique """ - user_request_session_key = self.user_request_session_key - return user_request_session_key + ":" + user_key + COMBINE_CHR = ":" + return self.user_request_session_key + COMBINE_CHR + user_key def set(self, *args, **kwargs): """ - let user easily set values + user should call this to easily set values eg. request.user.session.set(foo=123) + + pickle all values so that Redis can store any pickle-able Python value """ - # logger.debug(f"set RedisSession {args=} {kwargs=}") for user_key, value in kwargs.items(): actual_redis_key = self._actual_redis_key(user_key) # logger.debug(f"{user_key=} {actual_redis_key=} {value=}") - # self.r.set(actual_redis_key, value) self.r.set(actual_redis_key, pickle.dumps(value)) # def get(self, *args, **kwargs): @@ -49,17 +48,19 @@ def set(self, *args, **kwargs): def get(self, *args): """ - let user easily get values + user should call this to easily get values eg. request.user.session.get("foo") + + un-pickle all values that were retrieved from Redis """ user_key = args[0] actual_redis_key = self._actual_redis_key(user_key) # logger.debug(f"{user_key=} {actual_redis_key=}") - # return self.r.get(actual_redis_key) return pickle.loads(self.r.get(actual_redis_key)) +# FIXME this isn't used - can be removed? class RedisUser: def __init__(self, connection): # self.connection = connection @@ -78,7 +79,7 @@ async def on_startup(self, data): settings = data.server.settings - # FIXME FIXME FIXME + # FIXME # TODO what should the settings be? # TODO there are many kwargs (https://redis.readthedocs.io/en/latest/connections.html) # TODO maybe settings should be a dict to override the defaults? @@ -89,10 +90,11 @@ async def on_startup(self, data): # default args # self.redis_session = RedisSession(host="localhost", port=6379, db=0) - # FIXME pass kwargs (see note above) + # FIXME pass kwargs (see above) self.redis_session = RedisSession() - # to be set in handle_request() just prior to allowing user to call get(), set() + # to be set in handle_request() + # just prior to user calling request.user.session.get(), request.user.session.set() self.user_request_session_key = None return data From 4f964d6c5891d3f7dc1ac4a304671d9a96732107 Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 10:52:20 +1000 Subject: [PATCH 05/12] suggested settings in use --- example_redis.py | 13 ++++++++-- lona_redis/middlewares.py | 52 ++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/example_redis.py b/example_redis.py index c70e764..d188bb1 100644 --- a/example_redis.py +++ b/example_redis.py @@ -10,6 +10,9 @@ from loguru import logger +# NOTE start Redis before starting this lona script +# docker run -p 6379:6379 -it redis:latest --requirepass "abcd1234" + app = App(__file__) app.settings.SESSIONS = True @@ -17,8 +20,12 @@ "lona_redis.middlewares.RedisSessionMiddleware", ] -app.settings.REDIS_USER = "some_redis_user" -app.settings.REDIS_PASSWORD = "abcd1234" +# app.settings.REDIS_USER = "some_redis_user" +# app.settings.REDIS_PASSWORD = "abcd1234" + +# FIXME suggested way to pass Redis connection settings +# there are many many kwargs: https://redis.readthedocs.io/en/latest/connections.html +app.settings.REDIS_CONNECTION = {"password": "abcd1234"} @app.route("/") @@ -34,6 +41,8 @@ def handle_request(self, request): # request.user.session.set(foo=foo) # foo = request.user.session.get("foo") # logger.debug(f"{foo=}") + + # NOTE example of getting the actual redis key given user's key # logger.debug(f"{request.user.session._actual_redis_key('foo')=}") request.user.session.set(str_var="hello world") diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index 642fb9a..9e5d58e 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -14,13 +14,12 @@ def __init__(self, *args, **kwargs): initalize redis.Redis() https://redis.readthedocs.io/en/latest/#quickly-connecting-to-redis - https://redis.readthedocs.io/en/latest/examples/connection_examples.html - there are many many kwargs: + but there are many many kwargs: https://redis.readthedocs.io/en/latest/connections.html """ - # FIXME pass kwargs straight through to redis.Redis? + # FIXME suggest to pass kwargs through to redis.Redis self.r = redis.Redis(**kwargs) def _actual_redis_key(self, user_key): @@ -40,7 +39,6 @@ def set(self, *args, **kwargs): """ for user_key, value in kwargs.items(): actual_redis_key = self._actual_redis_key(user_key) - # logger.debug(f"{user_key=} {actual_redis_key=} {value=}") self.r.set(actual_redis_key, pickle.dumps(value)) # def get(self, *args, **kwargs): @@ -55,20 +53,18 @@ def get(self, *args): """ user_key = args[0] actual_redis_key = self._actual_redis_key(user_key) - # logger.debug(f"{user_key=} {actual_redis_key=}") - return pickle.loads(self.r.get(actual_redis_key)) # FIXME this isn't used - can be removed? -class RedisUser: - def __init__(self, connection): - # self.connection = connection - # self.session = RedisSession(self) - pass - - def __eq__(self, other): - raise NotImplementedError() +# class RedisUser: +# def __init__(self, connection): +# # self.connection = connection +# # self.session = RedisSession(self) +# pass +# +# def __eq__(self, other): +# raise NotImplementedError() class RedisSessionMiddleware: @@ -79,31 +75,31 @@ async def on_startup(self, data): settings = data.server.settings - # FIXME - # TODO what should the settings be? - # TODO there are many kwargs (https://redis.readthedocs.io/en/latest/connections.html) - # TODO maybe settings should be a dict to override the defaults? + # FIXME how to set Redis connection settings? + # there are many many kwargs (https://redis.readthedocs.io/en/latest/connections.html) + # FIXME maybe settings should be a dict instead # logger.debug(f"{settings.REDIS_USER=}") # logger.debug(f"{settings.REDIS_PASSWORD=}") - # default args - # self.redis_session = RedisSession(host="localhost", port=6379, db=0) + # common setting, using default args (ie. host="localhost", port=6379, db=0) + # self.redis_session = RedisSession() - # FIXME pass kwargs (see above) - self.redis_session = RedisSession() + # FIXME suggested way to pass & use Redis connection settings + # logger.debug(f"{settings.REDIS_CONNECTION=}") + self.redis_session = RedisSession(**settings.REDIS_CONNECTION) - # to be set in handle_request() + # initalize this here, but to be set in handle_request() # just prior to user calling request.user.session.get(), request.user.session.set() self.user_request_session_key = None return data - def handle_connection(self, data): - # connection.user = RedisUser(data.connection) - # logger.debug(f"{dir(data.connection)=}") - - return data + # FIXME this isn't used - can be removed? + # def handle_connection(self, data): + # # connection.user = RedisUser(data.connection) + # + # return data def handle_request(self, data): request = data.request From 8743b9fe705db26c0f07eba008c4a18e8aee147a Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 14:23:18 +1000 Subject: [PATCH 06/12] refactor, get multiple values at once --- lona_redis/middlewares.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index 9e5d58e..b7269f1 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -22,11 +22,12 @@ def __init__(self, *args, **kwargs): # FIXME suggest to pass kwargs through to redis.Redis self.r = redis.Redis(**kwargs) - def _actual_redis_key(self, user_key): + def redis_key(self, user_key): """ combine self.user_request_session_key with user's key so that the ACTUAL key used to store value in Redis is unique """ + COMBINE_CHR = ":" return self.user_request_session_key + COMBINE_CHR + user_key @@ -37,8 +38,9 @@ def set(self, *args, **kwargs): pickle all values so that Redis can store any pickle-able Python value """ + for user_key, value in kwargs.items(): - actual_redis_key = self._actual_redis_key(user_key) + actual_redis_key = self.redis_key(user_key) self.r.set(actual_redis_key, pickle.dumps(value)) # def get(self, *args, **kwargs): @@ -51,9 +53,19 @@ def get(self, *args): un-pickle all values that were retrieved from Redis """ - user_key = args[0] - actual_redis_key = self._actual_redis_key(user_key) - return pickle.loads(self.r.get(actual_redis_key)) + if len(args) == 1: + user_key = args[0] + actual_redis_key = self.redis_key(user_key) + return pickle.loads(self.r.get(actual_redis_key)) + + else: + values = () + for user_key in args: + actual_redis_key = self.redis_key(user_key) + value = pickle.loads(self.r.get(actual_redis_key)) + values = values + (value,) + + return values # FIXME this isn't used - can be removed? From fbc72876e84435cb1fed85a6047027f6d1086183 Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 14:24:02 +1000 Subject: [PATCH 07/12] more examples --- example_redis.py | 49 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/example_redis.py b/example_redis.py index d188bb1..5efe1cb 100644 --- a/example_redis.py +++ b/example_redis.py @@ -10,7 +10,7 @@ from loguru import logger -# NOTE start Redis before starting this lona script +# NOTE start Redis before starting this lona script, eg: # docker run -p 6379:6379 -it redis:latest --requirepass "abcd1234" app = App(__file__) @@ -25,26 +25,34 @@ # FIXME suggested way to pass Redis connection settings # there are many many kwargs: https://redis.readthedocs.io/en/latest/connections.html +# app.settings.REDIS_CONNECTION = {"password": "abcd1234", "decode_responses": False} app.settings.REDIS_CONNECTION = {"password": "abcd1234"} +# app.settings.REDIS_CONNECTION = {} @app.route("/") class Index(View): def handle_request(self, request): # + # NOTE THE "EASY" WAY # NOTE examples of using request.user.session.set, request.user.session.get # - # request.user.session.set(foo=123) - # OR - # foo = 123 - # request.user.session.set(foo=foo) - # foo = request.user.session.get("foo") - # logger.debug(f"{foo=}") + # NOTE set single value + request.user.session.set(foo=999) - # NOTE example of getting the actual redis key given user's key - # logger.debug(f"{request.user.session._actual_redis_key('foo')=}") + # NOTE get single value + foo = request.user.session.get("foo") + logger.debug(f"{foo=}") + # NOTE set multiple values + request.user.session.set(foo=123, bar=4.56, baz="hello world") + + # NOTE get multiple values + var_foo, var_bar, var_baz = request.user.session.get("foo", "bar", "baz") + logger.debug(f"{var_foo=}, {var_bar=}, {var_baz=}") + + # NOTE store various types of values request.user.session.set(str_var="hello world") request.user.session.set(list_var=[1, 2.222, "hello world"]) request.user.session.set(int_var=123) @@ -67,6 +75,7 @@ def handle_request(self, request): mixed_var_2={ "a": [1, 2, 3], "b": {"a": 1, "b": 2}, + "c": (4,5,6), } ) @@ -82,10 +91,30 @@ def handle_request(self, request): # # NOTE examples of using Redis commands directly - # # https://redis.readthedocs.io/en/latest/commands.html#core-commands + # + + # NOTE set key directly in Redis + # NOTE just an example, DON'T DO THIS + # should always use request.user.session.redis_key() + # Otherwise another session will overwrite this key request.user.session.r.set("myKey", "thevalueofmykey") myKey = request.user.session.r.get("myKey") + + # NOTE Set the value of key name to value if key doesn’t exist + # request.user.session.r.setnx(request.user.session.redis_key("count"), 1) + + # Increments the value of key by amount. If no key exists, the value will be initialized as amount + count = request.user.session.r.incr( + request.user.session.redis_key("count"), amount=1 + ) + + # NOTE when using r.get, value returned is in bytes - need to manage this yourself + count = request.user.session.r.get(request.user.session.redis_key("count")) + + logger.debug(f"{count=}") + + # NOTE show all keys all_keys = request.user.session.r.keys() logger.debug(f"{all_keys=}") From e8733ebd9ed1811299edffe4063e317d5c8032da Mon Sep 17 00:00:00 2001 From: SteveT Date: Thu, 20 Apr 2023 14:24:37 +1000 Subject: [PATCH 08/12] added more sections, examples --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index aec0226..1c004fd 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,57 @@ MIDDLEWARES = [ 'lona_redis.middlewares.RedisSessionMiddleware', ] ``` + +## What is Redis +Redis is key-value store. + +## What is lona-redis +lona-redis uses redis-py to let lona scripts store session data (key-values) in Redis. + +lona-redis also allows direct access to the Redis connection for direct execution of Redis commands + +## Start up Redis (using Docker) + +``` +docker run -p 6379:6379 -it redis:latest --requirepass "abcd1234" + +# without password +docker run -p 6379:6379 -it redis:latest +``` +or (https://developer.redis.com/create/docker/redis-on-docker/) +``` +docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest +``` + + +## Example lona script +```python +from lona import App, View +from lona.html import H1, HTML + +app = App(__file__) + +app.settings.SESSIONS = True +app.settings.MIDDLEWARES = [ + "lona_redis.middlewares.RedisSessionMiddleware", +] + +@app.route("/") +class Index(View): + def handle_request(self, request): + # set key, value + request.user.session.set(foo=123) + + # given key, get value + foo = request.user.session.get("foo") + + return HTML( + H1("Hello World"), + # P("Lorem Ipsum"), + ) + +``` + +## More examples of getting & setting session data +```python +``` \ No newline at end of file From cc59dec8c790b48932178ae4279a496360fc28c0 Mon Sep 17 00:00:00 2001 From: SteveT Date: Sat, 22 Apr 2023 09:28:52 +1000 Subject: [PATCH 09/12] accept *args for request.user.session.set, error check --- example_redis.py | 7 +++++-- lona_redis/middlewares.py | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/example_redis.py b/example_redis.py index 5efe1cb..16af777 100644 --- a/example_redis.py +++ b/example_redis.py @@ -39,7 +39,10 @@ def handle_request(self, request): # # NOTE set single value - request.user.session.set(foo=999) + # request.user.session.set(foo=999) + request.user.session.set("foo", 999) + # testing throw error + # request.user.session.set("foo", 999, 345) # NOTE get single value foo = request.user.session.get("foo") @@ -75,7 +78,7 @@ def handle_request(self, request): mixed_var_2={ "a": [1, 2, 3], "b": {"a": 1, "b": 2}, - "c": (4,5,6), + "c": (4, 5, 6), } ) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index b7269f1..00aa0ee 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -1,6 +1,8 @@ import pickle +import sys import redis +# FIXME remove this from loguru import logger @@ -34,12 +36,27 @@ def redis_key(self, user_key): def set(self, *args, **kwargs): """ user should call this to easily set values - eg. request.user.session.set(foo=123) + eg. request.user.session.set("foo",123) + or + eg. request.user.session.set(foo=123, bar="hello") pickle all values so that Redis can store any pickle-able Python value """ + if len(args) > 0: + # eg. request.user.session.set("foo",123) + if len(args) != 2: + class_name = self.__class__.__name__ + function_name = sys._getframe().f_code.co_name + raise TypeError( + f"{__name__}.{class_name}.{function_name} expected 2 arguments, got {len(args)}" + ) + else: + actual_redis_key = self.redis_key(args[0]) + self.r.set(actual_redis_key, pickle.dumps(args[1])) + for user_key, value in kwargs.items(): + # eg. request.user.session.set(foo=123, bar="hello") actual_redis_key = self.redis_key(user_key) self.r.set(actual_redis_key, pickle.dumps(value)) @@ -50,15 +67,20 @@ def get(self, *args): """ user should call this to easily get values eg. request.user.session.get("foo") + or + eg. request.user.session.get("foo", "bar", "baz") + return tuple of values un-pickle all values that were retrieved from Redis """ if len(args) == 1: + # eg. request.user.session.get("foo") user_key = args[0] actual_redis_key = self.redis_key(user_key) return pickle.loads(self.r.get(actual_redis_key)) else: + # eg. request.user.session.get("foo", "bar", "baz") values = () for user_key in args: actual_redis_key = self.redis_key(user_key) @@ -91,6 +113,7 @@ async def on_startup(self, data): # there are many many kwargs (https://redis.readthedocs.io/en/latest/connections.html) # FIXME maybe settings should be a dict instead + # FIXME remove this # logger.debug(f"{settings.REDIS_USER=}") # logger.debug(f"{settings.REDIS_PASSWORD=}") From 559806d02c47019dcf22e001aede6e6f943cbbcb Mon Sep 17 00:00:00 2001 From: SteveT Date: Sat, 22 Apr 2023 15:59:46 +1000 Subject: [PATCH 10/12] pass through kwargs for set(), get() args same as redis-py, added exists() --- lona_redis/middlewares.py | 98 ++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 54 deletions(-) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index 00aa0ee..bea57ba 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -2,8 +2,6 @@ import sys import redis -# FIXME remove this -from loguru import logger class RedisSession: @@ -17,11 +15,11 @@ def __init__(self, *args, **kwargs): https://redis.readthedocs.io/en/latest/#quickly-connecting-to-redis - but there are many many kwargs: - https://redis.readthedocs.io/en/latest/connections.html + pass through all kwargs to redis-py + kwargs: + https://redis.readthedocs.io/en/latest/connections.html """ - # FIXME suggest to pass kwargs through to redis.Redis self.r = redis.Redis(**kwargs) def redis_key(self, user_key): @@ -36,58 +34,61 @@ def redis_key(self, user_key): def set(self, *args, **kwargs): """ user should call this to easily set values - eg. request.user.session.set("foo",123) - or - eg. request.user.session.set(foo=123, bar="hello") - + eg. request.user.session.set("foo",123) pickle all values so that Redis can store any pickle-able Python value + + pass through all kwargs to redis-py .set() + kwargs: + https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.set """ - if len(args) > 0: - # eg. request.user.session.set("foo",123) - if len(args) != 2: - class_name = self.__class__.__name__ - function_name = sys._getframe().f_code.co_name - raise TypeError( - f"{__name__}.{class_name}.{function_name} expected 2 arguments, got {len(args)}" - ) - else: - actual_redis_key = self.redis_key(args[0]) - self.r.set(actual_redis_key, pickle.dumps(args[1])) + if len(args) == 2: + redis_key = self.redis_key(args[0]) + value = pickle.dumps(args[1]) - for user_key, value in kwargs.items(): - # eg. request.user.session.set(foo=123, bar="hello") - actual_redis_key = self.redis_key(user_key) - self.r.set(actual_redis_key, pickle.dumps(value)) + self.r.set(redis_key, value, **kwargs) - # def get(self, *args, **kwargs): - # raise NotImplementedError() + else: + class_name = self.__class__.__name__ + function_name = sys._getframe().f_code.co_name + raise TypeError( + f"{__name__}.{class_name}.{function_name} expected 2 arguments, got {len(args)}" + ) def get(self, *args): """ user should call this to easily get values - eg. request.user.session.get("foo") - or - eg. request.user.session.get("foo", "bar", "baz") - return tuple of values - + eg. request.user.session.get("foo") un-pickle all values that were retrieved from Redis + + if key does not exist, return None """ + if len(args) == 1: - # eg. request.user.session.get("foo") user_key = args[0] - actual_redis_key = self.redis_key(user_key) - return pickle.loads(self.r.get(actual_redis_key)) + if self.exists(user_key): + redis_key = self.redis_key(user_key) + return pickle.loads(self.r.get(redis_key)) + else: + return None else: - # eg. request.user.session.get("foo", "bar", "baz") - values = () - for user_key in args: - actual_redis_key = self.redis_key(user_key) - value = pickle.loads(self.r.get(actual_redis_key)) - values = values + (value,) + class_name = self.__class__.__name__ + function_name = sys._getframe().f_code.co_name + raise TypeError( + f"{__name__}.{class_name}.{function_name} expected 1 argument, got {len(args)}" + ) - return values + def exists(self, user_key): + """ + check if user_key exists + eg. request.user.session.exists("foo") + + return True/False + """ + + redis_key = self.redis_key(user_key) + return self.r.exists(redis_key) == 1 # FIXME this isn't used - can be removed? @@ -105,23 +106,12 @@ class RedisSessionMiddleware: async def on_startup(self, data): """ initalize Redis connection + + get settings from app.settings.REDIS_CONNECTION """ settings = data.server.settings - # FIXME how to set Redis connection settings? - # there are many many kwargs (https://redis.readthedocs.io/en/latest/connections.html) - # FIXME maybe settings should be a dict instead - - # FIXME remove this - # logger.debug(f"{settings.REDIS_USER=}") - # logger.debug(f"{settings.REDIS_PASSWORD=}") - - # common setting, using default args (ie. host="localhost", port=6379, db=0) - # self.redis_session = RedisSession() - - # FIXME suggested way to pass & use Redis connection settings - # logger.debug(f"{settings.REDIS_CONNECTION=}") self.redis_session = RedisSession(**settings.REDIS_CONNECTION) # initalize this here, but to be set in handle_request() From 77412f714924b2cde3fcf961e5682011514b6bf1 Mon Sep 17 00:00:00 2001 From: SteveT Date: Tue, 25 Apr 2023 17:20:21 +1000 Subject: [PATCH 11/12] updated README.md, updated middlewares.py set(), get(), exists() --- README.md | 80 +++++++++++++++++------ example_redis.py | 131 ++++++++++++++++++-------------------- lona_redis/middlewares.py | 25 +++++++- 3 files changed, 144 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 1c004fd..3a066f2 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,18 @@ ![Python Version](https://img.shields.io/pypi/pyversions/lona-redis.svg) ![Latest Version](https://img.shields.io/pypi/v/lona-redis.svg) +lona-redis uses Redis as a key-value store to store server side cookies. + +lona-redis also allows direct access to the Redis connection for direct execution of Redis commands. ## Installation -lona-picocss can be installed using pip +lona-redis can be installed using pip ``` pip install lona-redis ``` - ## Using Sessions ```python @@ -24,16 +26,8 @@ MIDDLEWARES = [ ] ``` -## What is Redis -Redis is key-value store. - -## What is lona-redis -lona-redis uses redis-py to let lona scripts store session data (key-values) in Redis. - -lona-redis also allows direct access to the Redis connection for direct execution of Redis commands - -## Start up Redis (using Docker) - +## Start up Redis +Using Docker ``` docker run -p 6379:6379 -it redis:latest --requirepass "abcd1234" @@ -45,7 +39,6 @@ or (https://developer.redis.com/create/docker/redis-on-docker/) docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest ``` - ## Example lona script ```python from lona import App, View @@ -57,23 +50,68 @@ app.settings.SESSIONS = True app.settings.MIDDLEWARES = [ "lona_redis.middlewares.RedisSessionMiddleware", ] +app.settings.REDIS_CONNECTION = {"password": "abcd1234"} @app.route("/") class Index(View): - def handle_request(self, request): - # set key, value - request.user.session.set(foo=123) + def handle_request(self, request): + session = request.user.session + + # set key, value + session.set("foo", 123) + + # returns True + session.exists("foo") - # given key, get value - foo = request.user.session.get("foo") + # given key, get value + foo = session.get("foo") - return HTML( + return HTML( H1("Hello World"), - # P("Lorem Ipsum"), ) +``` + +## Store other data types +Any data type that can be pickled can be stored +```python +session.set("str_var", "hello world") +session.set("list_var", [1, 2.222, "hello world"]) +session.set("int_var", 123) +session.set("float_var", 123.456) +session.set("tuple_var", (1, 2, 3)) +session.set("dict_var", {"a": 1, "b": 2}) +session.set("boolean_var", True) +session.set( + "mixed_types_var", + { + "a": [1, 2, 3], + "b": {"a": 1, "b": 2}, + "c": (4, 5, 6), + }, +) +``` +## Store key-values that expire +Arguments are passed through to Redis set() command +All parameters are available in lona-redis + +(https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.set) +```python +# "foo" will expire in 5 seconds +session.set("foo", 123, ex=5) ``` +### Using Redis commands directly +**Always access the key with session.redis_key()** -## More examples of getting & setting session data ```python +# Set the value of key name to value if key doesn’t exist +# https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.setnx +session.r.setnx(session.redis_key("count"), 1) + +# Increments the value of key by amount. If no key exists, the value will be initialized as amount +# https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.incr +count = session.r.incr(session.redis_key("count"), amount=1) + +# when using r.get, value returned is in bytes - need to manage this yourself +session.r.get(session.redis_key("count")) ``` \ No newline at end of file diff --git a/example_redis.py b/example_redis.py index 16af777..7310c5a 100644 --- a/example_redis.py +++ b/example_redis.py @@ -15,17 +15,13 @@ app = App(__file__) -app.settings.SESSIONS = True + +# app.settings.SESSIONS = True app.settings.MIDDLEWARES = [ "lona_redis.middlewares.RedisSessionMiddleware", ] -# app.settings.REDIS_USER = "some_redis_user" -# app.settings.REDIS_PASSWORD = "abcd1234" - -# FIXME suggested way to pass Redis connection settings -# there are many many kwargs: https://redis.readthedocs.io/en/latest/connections.html -# app.settings.REDIS_CONNECTION = {"password": "abcd1234", "decode_responses": False} +# Redis connection settings https://redis.readthedocs.io/en/latest/connections.html app.settings.REDIS_CONNECTION = {"password": "abcd1234"} # app.settings.REDIS_CONNECTION = {} @@ -37,60 +33,59 @@ def handle_request(self, request): # NOTE THE "EASY" WAY # NOTE examples of using request.user.session.set, request.user.session.get # + session = request.user.session + session.set("foo", 123) - # NOTE set single value - # request.user.session.set(foo=999) - request.user.session.set("foo", 999) - # testing throw error - # request.user.session.set("foo", 999, 345) - - # NOTE get single value - foo = request.user.session.get("foo") - logger.debug(f"{foo=}") + # NOTE set + session.set("foo", 123) + session.set("foo", 999, ex=5) - # NOTE set multiple values - request.user.session.set(foo=123, bar=4.56, baz="hello world") + # NOTE exists + logger.debug(f"{session.exists('foo')=}") + logger.debug(f"{session.exists('bar')=}") - # NOTE get multiple values - var_foo, var_bar, var_baz = request.user.session.get("foo", "bar", "baz") - logger.debug(f"{var_foo=}, {var_bar=}, {var_baz=}") + # NOTE get + logger.debug(f"{session.get('foo')=}") + logger.debug(f"{session.get('bar')=}") # NOTE store various types of values - request.user.session.set(str_var="hello world") - request.user.session.set(list_var=[1, 2.222, "hello world"]) - request.user.session.set(int_var=123) - request.user.session.set(float_var=123.456) - request.user.session.set(tuple_var=(1, 2, 3)) - request.user.session.set(dict_var={"a": 1, "b": 2}) - request.user.session.set(boolean_var=True) - request.user.session.set( - mixed_var_1=[ - True, - {"a": 1, "b": 2}, - (1, 2, 3), - 123.456, - 123, - [1, 2.222, "hello world"], - "hello world", - ] - ) - request.user.session.set( - mixed_var_2={ - "a": [1, 2, 3], - "b": {"a": 1, "b": 2}, - "c": (4, 5, 6), - } - ) - - logger.debug(f"{request.user.session.get('str_var')=}") - logger.debug(f"{request.user.session.get('list_var')=}") - logger.debug(f"{request.user.session.get('int_var')=}") - logger.debug(f"{request.user.session.get('float_var')=}") - logger.debug(f"{request.user.session.get('tuple_var')=}") - logger.debug(f"{request.user.session.get('dict_var')=}") - logger.debug(f"{request.user.session.get('boolean_var')=}") - logger.debug(f"{request.user.session.get('mixed_var_1')=}") - logger.debug(f"{request.user.session.get('mixed_var_2')=}") + session.set("str_var", "hello world") + session.set("list_var", [1, 2.222, "hello world"]) + session.set("int_var", 123) + session.set("float_var", 123.456) + session.set("tuple_var", (1, 2, 3)) + session.set("dict_var", {"a": 1, "b": 2}) + session.set("boolean_var", True) + # session.set( + # "mixed_var_1", + # [ + # True, + # {"a": 1, "b": 2}, + # (1, 2, 3), + # 123.456, + # 123, + # [1, 2.222, "hello world"], + # "hello world", + # ], + # ) + # request.user.session.set( + # "mixed_var_2", + # { + # "a": [1, 2, 3], + # "b": {"a": 1, "b": 2}, + # "c": (4, 5, 6), + # }, + # ) + + # logger.debug(f"{session.get('str_var')=}") + # logger.debug(f"{session.get('list_var')=}") + # logger.debug(f"{session.get('int_var')=}") + # logger.debug(f"{session.get('float_var')=}") + # logger.debug(f"{session.get('tuple_var')=}") + # logger.debug(f"{session.get('dict_var')=}") + # logger.debug(f"{session.get('boolean_var')=}") + # logger.debug(f"{session.get('mixed_var_1')=}") + # logger.debug(f"{session.get('mixed_var_2')=}") # # NOTE examples of using Redis commands directly @@ -99,32 +94,30 @@ def handle_request(self, request): # NOTE set key directly in Redis # NOTE just an example, DON'T DO THIS - # should always use request.user.session.redis_key() + # should always use session.redis_key() # Otherwise another session will overwrite this key - request.user.session.r.set("myKey", "thevalueofmykey") - myKey = request.user.session.r.get("myKey") + session.r.set("myKey", "thevalueofmykey") + myKey = session.r.get("myKey") # NOTE Set the value of key name to value if key doesn’t exist - # request.user.session.r.setnx(request.user.session.redis_key("count"), 1) + session.r.setnx(session.redis_key("count"), 1) # Increments the value of key by amount. If no key exists, the value will be initialized as amount - count = request.user.session.r.incr( - request.user.session.redis_key("count"), amount=1 - ) + count = session.r.incr(session.redis_key("count"), amount=1) + logger.debug(f"{count=}") # NOTE when using r.get, value returned is in bytes - need to manage this yourself - count = request.user.session.r.get(request.user.session.redis_key("count")) - + count = session.r.get(session.redis_key("count")) logger.debug(f"{count=}") # NOTE show all keys - all_keys = request.user.session.r.keys() - logger.debug(f"{all_keys=}") + logger.debug(f"{session.r.keys()=}") + + # getset + # count = session.set("count", 0, get=True) - # return HTML(H1(request.user.session["count"])) return HTML( H1("Hello World"), - # P("Lorem Ipsum"), ) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index bea57ba..c3a8b3b 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -15,11 +15,17 @@ def __init__(self, *args, **kwargs): https://redis.readthedocs.io/en/latest/#quickly-connecting-to-redis + kwargs is app.settings.REDIS_CONNECTION + pass through all kwargs to redis-py kwargs: https://redis.readthedocs.io/en/latest/connections.html """ + # called from RedisSessionMiddleware.on_startup() + + # .r is available by user for direct access to redis-py commands + # eg. all_keys = request.user.session.r.keys() self.r = redis.Redis(**kwargs) def redis_key(self, user_key): @@ -34,7 +40,7 @@ def redis_key(self, user_key): def set(self, *args, **kwargs): """ user should call this to easily set values - eg. request.user.session.set("foo",123) + eg. request.user.session.set("foo", 123) pickle all values so that Redis can store any pickle-able Python value pass through all kwargs to redis-py .set() @@ -45,7 +51,6 @@ def set(self, *args, **kwargs): if len(args) == 2: redis_key = self.redis_key(args[0]) value = pickle.dumps(args[1]) - self.r.set(redis_key, value, **kwargs) else: @@ -62,6 +67,9 @@ def get(self, *args): un-pickle all values that were retrieved from Redis if key does not exist, return None + + emulate Redis get() + https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.get """ if len(args) == 1: @@ -85,6 +93,11 @@ def exists(self, user_key): eg. request.user.session.exists("foo") return True/False + + as compared to: + https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.exists + + only accept 1 argument, return True/False """ redis_key = self.redis_key(user_key) @@ -127,7 +140,10 @@ async def on_startup(self, data): # return data def handle_request(self, data): + # server = data.server + # connection = data.connection request = data.request + # view = data.view # set self.user_request_session_key to request.user.session_key # so that subsequent calls by the user to @@ -136,6 +152,11 @@ def handle_request(self, data): # will have request.user.session_key available # we NEED this so that each Redis key is unique to request.user.session_key self.redis_session.user_request_session_key = request.user.session_key + + # eg. user will call request.user.session.get() + # request.user.session is set to self.redis_session + # which is initalised to RedisSession(**settings.REDIS_CONNECTION) + # where self.r = redis.Redis(**kwargs), kwargs=settings.REDIS_CONNECTION request.user.session = self.redis_session return data From 62cc58577036dfec7080318b6096ac10dba4e5cc Mon Sep 17 00:00:00 2001 From: SteveT Date: Tue, 25 Apr 2023 21:24:43 +1000 Subject: [PATCH 12/12] updated README.md, updated middlewares.py delete() --- README.md | 26 +++++++++----------------- example_redis.py | 9 +++++++-- lona_redis/middlewares.py | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3a066f2..4ca218d 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,6 @@ docker run -p 6379:6379 -it redis:latest --requirepass "abcd1234" # without password docker run -p 6379:6379 -it redis:latest ``` -or (https://developer.redis.com/create/docker/redis-on-docker/) -``` -docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest -``` ## Example lona script ```python @@ -57,14 +53,12 @@ class Index(View): def handle_request(self, request): session = request.user.session - # set key, value - session.set("foo", 123) - - # returns True - session.exists("foo") - - # given key, get value - foo = session.get("foo") + session.set("foo", 123) # set key, value + session.exists("foo") # returns True + foo = session.get("foo") # given key, get value + session.delete("foo") # returns True + session.delete("foo") # returns False, "foo" was already deleted + session.delete("bar") # returns False, "bar" doesn't exist return HTML( H1("Hello World"), @@ -91,16 +85,14 @@ session.set( ) ``` ## Store key-values that expire -Arguments are passed through to Redis set() command - -All parameters are available in lona-redis +All options are passed through to Redis set() command -(https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.set) +https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.set ```python # "foo" will expire in 5 seconds session.set("foo", 123, ex=5) ``` -### Using Redis commands directly +## Use Redis commands directly **Always access the key with session.redis_key()** ```python diff --git a/example_redis.py b/example_redis.py index 7310c5a..5b07df6 100644 --- a/example_redis.py +++ b/example_redis.py @@ -48,6 +48,11 @@ def handle_request(self, request): logger.debug(f"{session.get('foo')=}") logger.debug(f"{session.get('bar')=}") + # NOTE delete + logger.debug(f"{session.delete('foo')=}") + logger.debug(f"{session.delete('foo')=}") + logger.debug(f"{session.delete('bar')=}") + # NOTE store various types of values session.set("str_var", "hello world") session.set("list_var", [1, 2.222, "hello world"]) @@ -96,8 +101,8 @@ def handle_request(self, request): # NOTE just an example, DON'T DO THIS # should always use session.redis_key() # Otherwise another session will overwrite this key - session.r.set("myKey", "thevalueofmykey") - myKey = session.r.get("myKey") + # session.r.set("myKey", "thevalueofmykey") + # myKey = session.r.get("myKey") # NOTE Set the value of key name to value if key doesn’t exist session.r.setnx(session.redis_key("count"), 1) diff --git a/lona_redis/middlewares.py b/lona_redis/middlewares.py index c3a8b3b..652b0e4 100644 --- a/lona_redis/middlewares.py +++ b/lona_redis/middlewares.py @@ -103,6 +103,24 @@ def exists(self, user_key): redis_key = self.redis_key(user_key) return self.r.exists(redis_key) == 1 + def delete(self, user_key): + """ + delete user_key + eg. request.user.session.delete("foo") + + return + True if key existed and was deleted + False otherwise + + as compared to: + https://redis.readthedocs.io/en/latest/commands.html#redis.commands.core.CoreCommands.delete + + only accept 1 argument, return True/False + """ + + redis_key = self.redis_key(user_key) + return self.r.delete(redis_key) == 1 + # FIXME this isn't used - can be removed? # class RedisUser: