diff --git a/examples/python/README.md b/examples/python/README.md index 156627e6a3..d9e8960978 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -1,7 +1,7 @@ ## Run -To run the example or any other Python application utilizing Valkey GLIDE, activate the virtual environment that created by the 'Build' stage: ``` cd examples/python pip install -r requirements.txt -python3 client_example.py +python3 standalone_example.py +python3 cluster_example.py ``` diff --git a/examples/python/client_example.py b/examples/python/client_example.py deleted file mode 100755 index 35d9ea5a8b..0000000000 --- a/examples/python/client_example.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - -import asyncio -from typing import Optional, Union - -from glide import ( - AllNodes, - BaseClientConfiguration, - GlideClient, - GlideClusterClient, - Logger, - LogLevel, - NodeAddress, -) - - -def set_console_logger(level: LogLevel = LogLevel.WARN): - Logger.set_logger_config(level) - - -def set_file_logger(level: LogLevel = LogLevel.WARN, file: Optional[str] = None): - if file is None: - from datetime import datetime, timezone - - curr_time = datetime.now(timezone.utc) - curr_time_str = curr_time.strftime("%Y-%m-%dT%H:%M:%SZ") - file = f"{curr_time_str}-glide.log" - Logger.set_logger_config(level, file) - - -async def send_set_and_get(client: Union[GlideClient, GlideClusterClient]): - set_response = await client.set("foo", "bar") - print(f"Set response is = {set_response!r}") - get_response = await client.get("foo") - print(f"Get response is = {get_response!r}") - - -async def test_standalone_client(host: str = "localhost", port: int = 6379): - # When in Redis is in standalone mode, add address of the primary node, - # and any replicas you'd like to be able to read from. - addresses = [NodeAddress(host, port)] - # Check `GlideClientConfiguration/GlideClusterClientConfiguration` for additional options. - config = BaseClientConfiguration( - addresses=addresses, - client_name="test_standalone_client", - # if the server use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. - # use_tls=True - ) - client = await GlideClient.create(config) - - # Send SET and GET - await send_set_and_get(client) - # Send PING to the primary node - pong = await client.custom_command(["PING"]) - assert isinstance(pong, bytes) - print(f"PONG response is = {pong.decode()}") - - -async def test_cluster_client(host: str = "localhost", port: int = 6379): - # When in Redis is cluster mode, add address of any nodes, and the client will find all nodes in the cluster. - addresses = [NodeAddress(host, port)] - # Check `GlideClientConfiguration/GlideClusterClientConfiguration` for additional options. - config = BaseClientConfiguration( - addresses=addresses, - client_name="test_cluster_client", - # if the cluster nodes use TLS, you'll need to enable it. Otherwise the connection attempt will time out silently. - # use_tls=True - ) - client = await GlideClusterClient.create(config) - - # Send SET and GET - await send_set_and_get(client) - # Send PING to all primaries (according to Redis's PING request_policy) - pong = await client.custom_command(["PING"]) - assert isinstance(pong, bytes) - print(f"PONG response is = {pong.decode()}") - # Send INFO REPLICATION with routing option to all nodes - info_repl_resps = await client.custom_command(["INFO", "REPLICATION"], AllNodes()) - print(f"INFO REPLICATION responses from all nodes are = {info_repl_resps!r}") - - -async def main(): - set_console_logger(LogLevel.DEBUG) - set_file_logger(LogLevel.DEBUG) - await test_standalone_client() - await test_cluster_client() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/python/cluster_example.py b/examples/python/cluster_example.py new file mode 100644 index 0000000000..e26986bc87 --- /dev/null +++ b/examples/python/cluster_example.py @@ -0,0 +1,136 @@ +import asyncio +from typing import List, Tuple + +from glide import ( + AllNodes, + ClosingError, + ConnectionError, + GlideClusterClient, + GlideClusterClientConfiguration, + InfoSection, + Logger, + LogLevel, + NodeAddress, + RequestError, + TimeoutError, +) + + +async def create_client( + nodes_list: List[Tuple[str, int]] = [("localhost", 6379)] +) -> GlideClusterClient: + """ + Creates and returns a GlideClusterClient instance. + + This function initializes a GlideClusterClient with the provided list of nodes. + The nodes_list may contain the address of one or more cluster nodes, and the + client will automatically discover all nodes in the cluster. + + Args: + nodes_list (List[Tuple[str, int]]): A list of tuples where each tuple + contains a host (str) and port (int). Defaults to [("localhost", 6379)]. + + Returns: + GlideClusterClient: An instance of GlideClusterClient connected to the discovered nodes. + """ + addresses = [NodeAddress(host, port) for host, port in nodes_list] + # Check `GlideClusterClientConfiguration` for additional options. + config = GlideClusterClientConfiguration( + addresses=addresses, + client_name="test_cluster_client", + # Enable this field if the servers are configured with TLS. + # use_tls=True + ) + return await GlideClusterClient.create(config) + + +async def app_logic(client: GlideClusterClient): + """ + Executes the main logic of the application, performing basic operations + such as SET, GET, PING, and INFO REPLICATION using the provided GlideClusterClient. + + Args: + client (GlideClusterClient): An instance of GlideClusterClient. + """ + # Send SET and GET + set_response = await client.set("foo", "bar") + Logger.log(LogLevel.INFO, "app", f"Set response is = {set_response!r}") + + get_response = await client.get("foo") + assert isinstance(get_response, bytes) + Logger.log(LogLevel.INFO, "app", f"Get response is = {get_response.decode()!r}") + + # Send PING to all primaries (according to Redis's PING request_policy) + pong = await client.ping() + Logger.log(LogLevel.INFO, "app", f"PING response is = {pong!r}") + + # Send INFO REPLICATION with routing option to all nodes + info_repl_resps = await client.info([InfoSection.REPLICATION], AllNodes()) + Logger.log( + LogLevel.INFO, + "app", + f"INFO REPLICATION responses from all nodes are=\n{info_repl_resps!r}", + ) + + +async def exec_app_logic(): + """ + Executes the application logic with exception handling. + """ + while True: + try: + client = await create_client() + return await app_logic(client) + except asyncio.CancelledError: + raise + except ClosingError as e: + # If the error message contains "NOAUTH", raise the exception + # because it indicates a critical authentication issue. + if "NOAUTH" in str(e): + Logger.log( + LogLevel.ERROR, + "glide", + f"Authentication error encountered: {e}", + ) + raise e + Logger.log( + LogLevel.WARN, + "glide", + f"Client has closed and needs to be re-created: {e}", + ) + except TimeoutError as e: + # A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(LogLevel.ERROR, "glide", f"TimeoutError encountered: {e}") + raise e + except ConnectionError as e: + # The client wasn't able to reestablish the connection within the given retries + Logger.log(LogLevel.ERROR, "glide", f"ConnectionError encountered: {e}") + raise e + except RequestError as e: + # Other error reported during a request, such as a server response error + Logger.log(LogLevel.ERROR, "glide", f"RequestError encountered: {e}") + raise e + except Exception as e: + Logger.log(LogLevel.ERROR, "glide", f"Unexpected error: {e}") + raise e + finally: + try: + await client.close() + except Exception as e: + Logger.log( + LogLevel.WARN, + "glide", + f"Error encountered while closing the client: {e}", + ) + + +def main(): + # In this example, we will utilize the client's logger for all log messages + Logger.set_logger_config(LogLevel.INFO) + # Optional - set the logger to write to a file + # Logger.set_logger_config(LogLevel.INFO, file) + asyncio.run(exec_app_logic()) + + +if __name__ == "__main__": + main() diff --git a/examples/python/standalone_example.py b/examples/python/standalone_example.py new file mode 100644 index 0000000000..60e2b62dbb --- /dev/null +++ b/examples/python/standalone_example.py @@ -0,0 +1,129 @@ +import asyncio +from typing import List, Tuple + +from glide import ( + ClosingError, + ConnectionError, + GlideClient, + GlideClientConfiguration, + Logger, + LogLevel, + NodeAddress, + RequestError, + TimeoutError, +) + + +async def create_client( + nodes_list: List[Tuple[str, int]] = [("localhost", 6379)] +) -> GlideClient: + """ + Creates and returns a GlideClient instance. + + This function initializes a GlideClient with the provided list of nodes. + The nodes_list may contain either only primary node or a mix of primary + and replica nodes. The GlideClient use these nodes to connect to + the Standalone setup servers. + + Args: + nodes_list (List[Tuple[str, int]]): A list of tuples where each tuple + contains a host (str) and port (int). Defaults to [("localhost", 6379)]. + + Returns: + GlideClient: An instance of GlideClient connected to the specified nodes. + """ + addresses = [] + for host, port in nodes_list: + addresses.append(NodeAddress(host, port)) + + # Check `GlideClientConfiguration` for additional options. + config = GlideClientConfiguration( + addresses, + # Enable this field if the servers are configured with TLS. + # use_tls=True + ) + return await GlideClient.create(config) + + +async def app_logic(client: GlideClient): + """ + Executes the main logic of the application, performing basic operations + such as SET, GET, and PING using the provided GlideClient. + + Args: + client (GlideClient): An instance of GlideClient. + """ + # Send SET and GET + set_response = await client.set("foo", "bar") + Logger.log(LogLevel.INFO, "app", f"Set response is = {set_response!r}") + + get_response = await client.get("foo") + assert isinstance(get_response, bytes) + Logger.log(LogLevel.INFO, "app", f"Get response is = {get_response.decode()!r}") + + # Send PING to the primary node + pong = await client.ping() + Logger.log(LogLevel.INFO, "app", f"PING response is = {pong!r}") + + +async def exec_app_logic(): + """ + Executes the application logic with exception handling. + """ + while True: + try: + client = await create_client() + return await app_logic(client) + except asyncio.CancelledError: + raise + except ClosingError as e: + # If the error message contains "NOAUTH", raise the exception + # because it indicates a critical authentication issue. + if "NOAUTH" in str(e): + Logger.log( + LogLevel.ERROR, + "glide", + f"Authentication error encountered: {e}", + ) + raise e + Logger.log( + LogLevel.WARN, + "glide", + f"Client has closed and needs to be re-created: {e}", + ) + except TimeoutError as e: + # A request timed out. You may choose to retry the execution based on your application's logic + Logger.log(LogLevel.ERROR, "glide", f"TimeoutError encountered: {e}") + raise e + except ConnectionError as e: + # The client wasn't able to reestablish the connection within the given retries + Logger.log(LogLevel.ERROR, "glide", f"ConnectionError encountered: {e}") + raise e + except RequestError as e: + # Other error reported during a request, such as a server response error + Logger.log(LogLevel.ERROR, "glide", f"RequestError encountered: {e}") + raise e + except Exception as e: + Logger.log(LogLevel.ERROR, "glide", f"Unexpected error: {e}") + raise e + finally: + try: + await client.close() + except Exception as e: + Logger.log( + LogLevel.WARN, + "glide", + f"Encountered an error while closing the client: {e}", + ) + + +def main(): + # In this example, we will utilize the client's logger for all log messages + Logger.set_logger_config(LogLevel.INFO) + # Optional - set the logger to write to a file + # Logger.set_logger_config(LogLevel.INFO, file) + asyncio.run(exec_app_logic()) + + +if __name__ == "__main__": + main() diff --git a/python/README.md b/python/README.md index e1b2954ef7..89c4bec560 100644 --- a/python/README.md +++ b/python/README.md @@ -82,6 +82,8 @@ Set response is OK Get response is bar ``` +For complete examples with error handling, please refer to the [cluster example](https://github.com/valkey-io/valkey-glide/blob/main/examples/python/cluster_example.py) and the [standalone example](https://github.com/valkey-io/valkey-glide/blob/main/examples/python/standalone_example.py). + ## Documentation Visit our [wiki](https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper) for examples and further details on TLS, Read strategy, Timeouts and various other configurations.