diff --git a/benchmarks/python/python_benchmark.py b/benchmarks/python/python_benchmark.py index aea7fdbd4a..3f81b4de03 100644 --- a/benchmarks/python/python_benchmark.py +++ b/benchmarks/python/python_benchmark.py @@ -17,6 +17,7 @@ import redis.asyncio as redispy # type: ignore from glide import ( GlideClient, + TGlideClient, GlideClientConfiguration, GlideClusterClient, GlideClusterClientConfiguration, diff --git a/docs/markdown/python/base_batch.md b/docs/markdown/python/base_batch.md index 9f5ec1fa91..1387ad1742 100644 --- a/docs/markdown/python/base_batch.md +++ b/docs/markdown/python/base_batch.md @@ -1 +1 @@ -::: glide.async_commands.batch.BaseBatch +::: glide.commands.batch.BaseBatch diff --git a/docs/markdown/python/cluster_batch.md b/docs/markdown/python/cluster_batch.md index 3584ff84c7..9d440e6135 100644 --- a/docs/markdown/python/cluster_batch.md +++ b/docs/markdown/python/cluster_batch.md @@ -1 +1 @@ -::: glide.async_commands.batch.ClusterBatch +::: glide.commands.batch.ClusterBatch diff --git a/docs/markdown/python/cluster_transaction.md b/docs/markdown/python/cluster_transaction.md index 9e863652a5..316ccafb79 100644 --- a/docs/markdown/python/cluster_transaction.md +++ b/docs/markdown/python/cluster_transaction.md @@ -1 +1 @@ -::: glide.async_commands.batch.ClusterTransaction +::: glide.commands.batch.ClusterTransaction diff --git a/docs/markdown/python/standalone_batch.md b/docs/markdown/python/standalone_batch.md index 6ddfc6b008..26af3e5c13 100644 --- a/docs/markdown/python/standalone_batch.md +++ b/docs/markdown/python/standalone_batch.md @@ -1 +1 @@ -::: glide.async_commands.batch.Batch +::: glide.commands.batch.Batch diff --git a/docs/markdown/python/standalone_transaction.md b/docs/markdown/python/standalone_transaction.md index 2961de93cf..235a77d773 100644 --- a/docs/markdown/python/standalone_transaction.md +++ b/docs/markdown/python/standalone_transaction.md @@ -1 +1 @@ -::: glide.async_commands.batch.Transaction +::: glide.commands.batch.Transaction diff --git a/examples/python/ft_example.py b/examples/python/ft_example.py index 0dce40c6ac..9d55a0a060 100644 --- a/examples/python/ft_example.py +++ b/examples/python/ft_example.py @@ -15,17 +15,17 @@ RequestError, ) from glide import TimeoutError as GlideTimeoutError -from glide.async_commands.server_modules import ft, glide_json -from glide.async_commands.server_modules.ft_options.ft_create_options import ( +from glide.shared.commands.server_modules import ft, glide_json +from glide.shared.commands.server_modules.ft_options.ft_create_options import ( DataType, FtCreateOptions, NumericField, ) -from glide.async_commands.server_modules.ft_options.ft_search_options import ( +from glide.shared.commands.server_modules.ft_options.ft_search_options import ( FtSearchOptions, ReturnField, ) -from glide.constants import OK, FtSearchResponse, TEncodable +from glide.shared.constants import OK, FtSearchResponse, TEncodable async def create_client( diff --git a/examples/python/json_example.py b/examples/python/json_example.py index 3e37069dca..1126a5efc5 100644 --- a/examples/python/json_example.py +++ b/examples/python/json_example.py @@ -14,7 +14,7 @@ RequestError, ) from glide import TimeoutError as GlideTimeoutError -from glide.async_commands.server_modules import glide_json +from glide.shared.commands.server_modules import glide_json async def create_client( diff --git a/ffi/.cargo/config.toml b/ffi/.cargo/config.toml index 89e44b8095..48a827ca67 100644 --- a/ffi/.cargo/config.toml +++ b/ffi/.cargo/config.toml @@ -1,5 +1,5 @@ [env] -GLIDE_NAME = { value = "GlideFFI", force = true } +GLIDE_NAME = { value = "GlideGo", force = true } GLIDE_VERSION = "0.1.0" # Suppress error # > ... was built for newer 'macOS' version (14.5) than being linked (14.0) diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 5b81f7338e..648e9ce2fe 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" authors = ["Valkey GLIDE Maintainers"] [lib] -crate-type = ["staticlib", "rlib"] +crate-type = ["staticlib", "rlib", "cdylib"] [dependencies] protobuf = { version = "3", features = [] } diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index fd0ef3352a..c8eeb456ba 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -936,7 +936,7 @@ pub unsafe extern "C" fn command( }; // Create the command outside of the task to ensure that the command arguments passed - // from "go" are still valid + // from the caller are still valid let mut cmd = command_type .get_command() .expect("Couldn't fetch command type"); @@ -950,7 +950,7 @@ pub unsafe extern "C" fn command( } else { Routes::default() }; - + let mut client = client_adapter.core.client.clone(); client_adapter.execute_command(channel, async move { client diff --git a/python/dev.py b/python/dev.py index 50d01caa2d..237d1a2be3 100755 --- a/python/dev.py +++ b/python/dev.py @@ -18,13 +18,16 @@ def find_project_root() -> Path: # Constants PROTO_REL_PATH = "glide-core/src/protobuf" -PYTHON_CLIENT_PATH = "python/python/glide" VENV_NAME = ".env" GLIDE_ROOT = find_project_root() PYTHON_DIR = GLIDE_ROOT / "python" -VENV_DIR = PYTHON_DIR / VENV_NAME +PYTHON_CLIENT_PATH = PYTHON_DIR / "glide" +ASYNC_CLIENT_DIR = PYTHON_CLIENT_PATH / "glide_async" +SYNC_CLIENT_DIR = PYTHON_CLIENT_PATH / "glide_sync" +VENV_DIR = ASYNC_CLIENT_DIR / VENV_NAME VENV_BIN_DIR = VENV_DIR / "bin" PYTHON_EXE = VENV_BIN_DIR / "python" +FFI_DIR = GLIDE_ROOT / "ffi" def check_dependencies() -> None: @@ -95,7 +98,7 @@ def get_venv_env() -> Dict[str, str]: def generate_protobuf_files() -> None: proto_src = GLIDE_ROOT / PROTO_REL_PATH - proto_dst = GLIDE_ROOT / PYTHON_CLIENT_PATH + proto_dst = PYTHON_CLIENT_PATH proto_files = list(proto_src.glob("*.proto")) if not proto_files: @@ -139,10 +142,19 @@ def build_async_client(release: bool, no_cache: bool = False) -> None: if release: cmd += ["--release", "--strip"] - run_command(cmd, cwd=PYTHON_DIR, env=env, label="maturin develop") + run_command(cmd, cwd=ASYNC_CLIENT_DIR, env=env, label="maturin develop") print("[OK] Async client build completed") +def build_sync_client() -> None: + print("[INFO] Building sync client...") + generate_protobuf_files() + + run_command(["cargo", "build"], cwd=FFI_DIR, label="cargo build ffi") + + print("[OK] Sync client build completed") + + def run_command( cmd: List[str], cwd: Optional[Path] = None, @@ -220,6 +232,7 @@ def main() -> None: Examples: python dev.py build # Build the async client in debug mode python dev.py build --client async --mode release # Build the async client in release mode + python dev.py build --client sync # Build the sync client python dev.py protobuf # Generate Python protobuf files (.py and .pyi) python dev.py lint # Run Python linters python dev.py test # Run all tests @@ -231,7 +244,10 @@ def main() -> None: build_parser = subparsers.add_parser("build", help="Build the Python clients") build_parser.add_argument( - "--client", default="async", choices=["async"], help="Which client to build" + "--client", + default="all", + choices=["async", "sync", "all"], + help="Which client to build: 'async', 'sync', or 'all' to build both.", ) build_parser.add_argument( "--mode", choices=["debug", "release"], default="debug", help="Build mode" @@ -279,9 +295,12 @@ def main() -> None: elif args.command == "build": release = args.mode == "release" no_cache = args.no_cache - if args.client in ("async"): + if args.client in ["async", "all"]: print(f"🛠 Building async client ({args.mode} mode)...") build_async_client(release, no_cache) + if args.client in ["sync", "all"]: + print("🛠 Building sync client...") + build_sync_client() print("[✅ DONE] Task completed successfully.") diff --git a/python/dev_requirements.txt b/python/dev_requirements.txt index 38ca802b07..27fd6f10f8 100644 --- a/python/dev_requirements.txt +++ b/python/dev_requirements.txt @@ -13,3 +13,7 @@ sphinx >= 7.4.7 sphinx-rtd-theme deprecated types-Deprecated + +# Sync +cffi +types-cffi diff --git a/python/docs/conf.py b/python/docs/conf.py index 845a7491d0..d28bc9daef 100644 --- a/python/docs/conf.py +++ b/python/docs/conf.py @@ -31,6 +31,7 @@ "glide.protobuf", "pytest", "google", + "cffi", ] # Prevents confusion in sphinx with imports autodoc_typehints = "description" diff --git a/python/docs/index.rst b/python/docs/index.rst index c0608cda9f..65b2ed1e41 100644 --- a/python/docs/index.rst +++ b/python/docs/index.rst @@ -13,5 +13,5 @@ Welcome to the documentation page for Valkey GLIDE! :caption: Contents: glide - glide.async_commands + glide.commands.async_commands modules diff --git a/python/Cargo.toml b/python/glide/glide_async/Cargo.toml similarity index 75% rename from python/Cargo.toml rename to python/glide/glide_async/Cargo.toml index 0a57270157..1f545ab030 100644 --- a/python/Cargo.toml +++ b/python/glide/glide_async/Cargo.toml @@ -13,14 +13,14 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "^0.24", features = ["extension-module", "num-bigint"] } bytes = { version = "^1.8" } -redis = { path = "../glide-core/redis-rs/redis", features = [ +redis = { path = "../../../glide-core/redis-rs/redis", features = [ "aio", "tokio-comp", "connection-manager", "tokio-rustls-comp", ] } -glide-core = { path = "../glide-core", features = ["socket-layer"] } -logger_core = { path = "../logger_core" } +glide-core = { path = "../../../glide-core", features = ["socket-layer"] } +logger_core = { path = "../../../logger_core" } [package.metadata.maturin] python-source = "python" diff --git a/python/glide/glide_async/pyproject.toml b/python/glide/glide_async/pyproject.toml new file mode 100644 index 0000000000..d7671f1902 --- /dev/null +++ b/python/glide/glide_async/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["maturin==0.14.17"] +build-backend = "maturin" + +[project] +name = "test-glide-async" +description = "An open source Valkey client library that supports Valkey and Redis open source 6.2, 7.0, 7.2 and 8.0." +requires-python = ">=3.9" +dependencies = [ + # Note: If you add a dependency here, make sure to also add it to requirements.txt + # Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, the requirements.txt file can be removed. + "async-timeout>=4.0.2; python_version < '3.11'", + "typing-extensions>=4.8.0; python_version < '3.11'", + "protobuf>=3.20", +] + +classifiers = [ + "Topic :: Database", + "Topic :: Utilities", + "License :: OSI Approved :: Apache Software License", + "Intended Audience :: Developers", + "Topic :: Software Development", + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + +[tool.isort] +profile = "black" +skip = [ + ".env", + "python/glide/protobuf" +] + +[tool.black] +target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] + +[tool.mypy] +exclude = "^(.*\\/)?(\\.env|python/python/glide/protobuf|utils/release-candidate-testing|target|ort)(\\/|$)" diff --git a/python/glide/glide_async/python/glide/__init__.py b/python/glide/glide_async/python/glide/__init__.py new file mode 100644 index 0000000000..2e0e61be00 --- /dev/null +++ b/python/glide/glide_async/python/glide/__init__.py @@ -0,0 +1,5 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from .glide_client import BaseClient, GlideClient, GlideClusterClient, TGlideClient + +__all__ = ["BaseClient", "GlideClient", "GlideClusterClient", "TGlideClient"] diff --git a/python/python/glide/async_commands/__init__.py b/python/glide/glide_async/python/glide/async_commands/__init__.py similarity index 100% rename from python/python/glide/async_commands/__init__.py rename to python/glide/glide_async/python/glide/async_commands/__init__.py diff --git a/python/python/glide/async_commands/cluster_commands.py b/python/glide/glide_async/python/glide/async_commands/cluster_commands.py similarity index 99% rename from python/python/glide/async_commands/cluster_commands.py rename to python/glide/glide_async/python/glide/async_commands/cluster_commands.py index 8f281b02db..a28977bb4c 100644 --- a/python/python/glide/async_commands/cluster_commands.py +++ b/python/glide/glide_async/python/glide/async_commands/cluster_commands.py @@ -4,15 +4,11 @@ from typing import Dict, List, Mapping, Optional, Union, cast -from glide.async_commands.batch import ClusterBatch -from glide.async_commands.command_args import ObjectType -from glide.async_commands.core import ( - CoreCommands, - FlushMode, - FunctionRestorePolicy, - InfoSection, -) -from glide.constants import ( +from ..async_commands.core import CoreCommands +from glide.shared.commands.batch import ClusterBatch +from glide.shared.commands.command_args import ObjectType +from glide.shared.commands.core_options import FlushMode, FunctionRestorePolicy, InfoSection +from glide.shared.constants import ( TOK, TClusterResponse, TEncodable, @@ -21,10 +17,10 @@ TResult, TSingleNodeRoute, ) -from glide.protobuf.command_request_pb2 import RequestType -from glide.routes import Route +from glide.shared.protobuf.command_request_pb2 import RequestType +from glide.shared.routes import Route -from ..glide import ClusterScanCursor, Script +from glide.glide_async.python.glide.glide import ClusterScanCursor, Script class ClusterCommands(CoreCommands): diff --git a/python/python/glide/async_commands/core.py b/python/glide/glide_async/python/glide/async_commands/core.py similarity index 96% rename from python/python/glide/async_commands/core.py rename to python/glide/glide_async/python/glide/async_commands/core.py index dc891febe9..c56f7043a0 100644 --- a/python/python/glide/async_commands/core.py +++ b/python/glide/glide_async/python/glide/async_commands/core.py @@ -1,22 +1,7 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from dataclasses import dataclass -from datetime import datetime, timedelta -from enum import Enum -from typing import ( - Dict, - List, - Mapping, - Optional, - Protocol, - Set, - Tuple, - Type, - Union, - cast, - get_args, -) +from typing import Dict, List, Mapping, Optional, Protocol, Set, Tuple, Union, cast -from glide.async_commands.bitmap import ( +from glide.shared.commands.bitmap import ( BitFieldGet, BitFieldSubCommands, BitwiseOperation, @@ -24,8 +9,19 @@ _create_bitfield_args, _create_bitfield_read_only_args, ) -from glide.async_commands.command_args import Limit, ListDirection, ObjectType, OrderBy -from glide.async_commands.sorted_set import ( +from glide.shared.commands.command_args import Limit, ListDirection, ObjectType, OrderBy +from glide.shared.commands.core_options import ( + ConditionalChange, + ExpireOptions, + ExpiryGetEx, + ExpirySet, + InsertPosition, + OnlyIfEqual, + PubSubMsg, + UpdateOptions, + _build_sort_args, +) +from glide.shared.commands.sorted_set import ( AggregationType, GeoSearchByBox, GeoSearchByRadius, @@ -43,7 +39,7 @@ _create_zinter_zunion_cmd_args, _create_zrange_args, ) -from glide.async_commands.stream import ( +from glide.shared.commands.stream import ( StreamAddOptions, StreamClaimOptions, StreamGroupOptions, @@ -54,388 +50,17 @@ StreamTrimOptions, _create_xpending_range_args, ) -from glide.constants import ( +from glide.shared.constants import ( TOK, TEncodable, TResult, TXInfoStreamFullResponse, TXInfoStreamResponse, ) -from glide.protobuf.command_request_pb2 import RequestType -from glide.routes import Route - -from ..glide import ClusterScanCursor - - -class ConditionalChange(Enum): - """ - A condition to the `SET`, `ZADD` and `GEOADD` commands. - """ - - ONLY_IF_EXISTS = "XX" - """ Only update key / elements that already exist. Equivalent to `XX` in the Valkey API. """ - - ONLY_IF_DOES_NOT_EXIST = "NX" - """ Only set key / add elements that does not already exist. Equivalent to `NX` in the Valkey API. """ - - -@dataclass -class OnlyIfEqual: - """ - Change condition to the `SET` command, - For additional conditonal options see ConditionalChange - - - comparison_value - value to compare to the current value of a key. - - If comparison_value is equal to the key, it will overwrite the value of key to the new provided value - Equivalent to the IFEQ comparison-value in the Valkey API - """ - - comparison_value: TEncodable - - -class ExpiryType(Enum): - """ - SET option: The type of the expiry. - """ - - SEC = 0, Union[int, timedelta] - """ - Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. - """ - - MILLSEC = 1, Union[int, timedelta] - """ - Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. - """ - - UNIX_SEC = 2, Union[int, datetime] - """ - Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. - """ - - UNIX_MILLSEC = 3, Union[int, datetime] - """ - Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Valkey API. - """ - - KEEP_TTL = 4, Type[None] - """ - Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Valkey API. - """ - - -class ExpiryTypeGetEx(Enum): - """ - GetEx option: The type of the expiry. - """ - - SEC = 0, Union[int, timedelta] - """ Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. """ - - MILLSEC = 1, Union[int, timedelta] - """ Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. """ - - UNIX_SEC = 2, Union[int, datetime] - """ Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. """ - - UNIX_MILLSEC = 3, Union[int, datetime] - """ Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Valkey API. """ - - PERSIST = 4, Type[None] - """ Remove the time to live associated with the key. Equivalent to `PERSIST` in the Valkey API. """ - - -class InfoSection(Enum): - """ - INFO option: a specific section of information: - - When no parameter is provided, the default option is assumed. - """ - - SERVER = "server" - """ General information about the server """ - - CLIENTS = "clients" - """ Client connections section """ - - MEMORY = "memory" - """ Memory consumption related information """ - - PERSISTENCE = "persistence" - """ RDB and AOF related information """ - - STATS = "stats" - """ General statistics """ - - REPLICATION = "replication" - """ Master/replica replication information """ - - CPU = "cpu" - """ CPU consumption statistics """ - - COMMAND_STATS = "commandstats" - """ Valkey command statistics """ - - LATENCY_STATS = "latencystats" - """ Valkey command latency percentile distribution statistics """ - - SENTINEL = "sentinel" - """ Valkey Sentinel section (only applicable to Sentinel instances) """ - - CLUSTER = "cluster" - """ Valkey Cluster section """ - - MODULES = "modules" - """ Modules section """ - - KEYSPACE = "keyspace" - """ Database related statistics """ - - ERROR_STATS = "errorstats" - """ Valkey error statistics """ - - ALL = "all" - """ Return all sections (excluding module generated ones) """ - - DEFAULT = "default" - """ Return only the default set of sections """ - - EVERYTHING = "everything" - """ Includes all and modules """ +from glide.shared.protobuf.command_request_pb2 import RequestType +from glide.shared.routes import Route - -class ExpireOptions(Enum): - """ - EXPIRE option: options for setting key expiry. - """ - - HasNoExpiry = "NX" - """ Set expiry only when the key has no expiry (Equivalent to "NX" in Valkey). """ - - HasExistingExpiry = "XX" - """ Set expiry only when the key has an existing expiry (Equivalent to "XX" in Valkey). """ - - NewExpiryGreaterThanCurrent = "GT" - """ - Set expiry only when the new expiry is greater than the current one (Equivalent to "GT" in Valkey). - """ - - NewExpiryLessThanCurrent = "LT" - """ - Set expiry only when the new expiry is less than the current one (Equivalent to "LT" in Valkey). - """ - - -class UpdateOptions(Enum): - """ - Options for updating elements of a sorted set key. - """ - - LESS_THAN = "LT" - """ Only update existing elements if the new score is less than the current score. """ - - GREATER_THAN = "GT" - """ Only update existing elements if the new score is greater than the current score. """ - - -class ExpirySet: - """ - SET option: Represents the expiry type and value to be executed with "SET" command. - - Attributes: - cmd_arg (str): The expiry type. - value (str): The value for the expiry type. - """ - - def __init__( - self, - expiry_type: ExpiryType, - value: Optional[Union[int, datetime, timedelta]], - ) -> None: - self.set_expiry_type_and_value(expiry_type, value) - - def __eq__(self, other: "object") -> bool: - if not isinstance(other, ExpirySet): - return NotImplemented - return self.expiry_type == other.expiry_type and self.value == other.value - - def set_expiry_type_and_value( - self, expiry_type: ExpiryType, value: Optional[Union[int, datetime, timedelta]] - ): - """ - Args: - expiry_type (ExpiryType): The expiry type. - value (Optional[Union[int, datetime, timedelta]]): The value of the expiration type. The type of expiration - determines the type of expiration value: - - - SEC: Union[int, timedelta] - - MILLSEC: Union[int, timedelta] - - UNIX_SEC: Union[int, datetime] - - UNIX_MILLSEC: Union[int, datetime] - - KEEP_TTL: Type[None] - """ - if not isinstance(value, get_args(expiry_type.value[1])): - raise ValueError( - f"The value of {expiry_type} should be of type {expiry_type.value[1]}" - ) - self.expiry_type = expiry_type - if self.expiry_type == ExpiryType.SEC: - self.cmd_arg = "EX" - if isinstance(value, timedelta): - value = int(value.total_seconds()) - elif self.expiry_type == ExpiryType.MILLSEC: - self.cmd_arg = "PX" - if isinstance(value, timedelta): - value = int(value.total_seconds() * 1000) - elif self.expiry_type == ExpiryType.UNIX_SEC: - self.cmd_arg = "EXAT" - if isinstance(value, datetime): - value = int(value.timestamp()) - elif self.expiry_type == ExpiryType.UNIX_MILLSEC: - self.cmd_arg = "PXAT" - if isinstance(value, datetime): - value = int(value.timestamp() * 1000) - elif self.expiry_type == ExpiryType.KEEP_TTL: - self.cmd_arg = "KEEPTTL" - self.value = str(value) if value else None - - def get_cmd_args(self) -> List[str]: - return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] - - -class ExpiryGetEx: - """ - GetEx option: Represents the expiry type and value to be executed with "GetEx" command. - - Attributes: - cmd_arg (str): The expiry type. - value (str): The value for the expiry type. - """ - - def __init__( - self, - expiry_type: ExpiryTypeGetEx, - value: Optional[Union[int, datetime, timedelta]], - ) -> None: - self.set_expiry_type_and_value(expiry_type, value) - - def set_expiry_type_and_value( - self, - expiry_type: ExpiryTypeGetEx, - value: Optional[Union[int, datetime, timedelta]], - ): - """ - Args: - expiry_type (ExpiryType): The expiry type. - value (Optional[Union[int, datetime, timedelta]]): The value of the expiration type. The type of expiration - determines the type of expiration value: - - - SEC: Union[int, timedelta] - - MILLSEC: Union[int, timedelta] - - UNIX_SEC: Union[int, datetime] - - UNIX_MILLSEC: Union[int, datetime] - - PERSIST: Type[None] - """ - if not isinstance(value, get_args(expiry_type.value[1])): - raise ValueError( - f"The value of {expiry_type} should be of type {expiry_type.value[1]}" - ) - self.expiry_type = expiry_type - if self.expiry_type == ExpiryTypeGetEx.SEC: - self.cmd_arg = "EX" - if isinstance(value, timedelta): - value = int(value.total_seconds()) - elif self.expiry_type == ExpiryTypeGetEx.MILLSEC: - self.cmd_arg = "PX" - if isinstance(value, timedelta): - value = int(value.total_seconds() * 1000) - elif self.expiry_type == ExpiryTypeGetEx.UNIX_SEC: - self.cmd_arg = "EXAT" - if isinstance(value, datetime): - value = int(value.timestamp()) - elif self.expiry_type == ExpiryTypeGetEx.UNIX_MILLSEC: - self.cmd_arg = "PXAT" - if isinstance(value, datetime): - value = int(value.timestamp() * 1000) - elif self.expiry_type == ExpiryTypeGetEx.PERSIST: - self.cmd_arg = "PERSIST" - self.value = str(value) if value else None - - def get_cmd_args(self) -> List[str]: - return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] - - -class InsertPosition(Enum): - BEFORE = "BEFORE" - AFTER = "AFTER" - - -class FlushMode(Enum): - """ - Defines flushing mode for: - - `FLUSHALL` command and `FUNCTION FLUSH` command. - - See [FLUSHAL](https://valkey.io/commands/flushall/) and [FUNCTION-FLUSH](https://valkey.io/commands/function-flush/) - for details - - SYNC was introduced in version 6.2.0. - """ - - ASYNC = "ASYNC" - SYNC = "SYNC" - - -class FunctionRestorePolicy(Enum): - """ - Options for the FUNCTION RESTORE command. - """ - - APPEND = "APPEND" - """ Appends the restored libraries to the existing libraries and aborts on collision. This is the default policy. """ - - FLUSH = "FLUSH" - """ Deletes all existing libraries before restoring the payload. """ - - REPLACE = "REPLACE" - """ - Appends the restored libraries to the existing libraries, replacing any existing ones in case - of name collisions. Note that this policy doesn't prevent function name collisions, only libraries. - """ - - -def _build_sort_args( - key: TEncodable, - by_pattern: Optional[TEncodable] = None, - limit: Optional[Limit] = None, - get_patterns: Optional[List[TEncodable]] = None, - order: Optional[OrderBy] = None, - alpha: Optional[bool] = None, - store: Optional[TEncodable] = None, -) -> List[TEncodable]: - args = [key] - - if by_pattern: - args.extend(["BY", by_pattern]) - - if limit: - args.extend(["LIMIT", str(limit.offset), str(limit.count)]) - - if get_patterns: - for pattern in get_patterns: - args.extend(["GET", pattern]) - - if order: - args.append(order.value) - - if alpha: - args.append("ALPHA") - - if store: - args.extend(["STORE", store]) - - return args +from glide.glide_async.python.glide.glide import ClusterScanCursor class CoreCommands(Protocol): @@ -7022,21 +6647,6 @@ async def watch(self, keys: List[TEncodable]) -> TOK: await self._execute_command(RequestType.Watch, keys), ) - @dataclass - class PubSubMsg: - """ - Describes the incoming pubsub message - - Attributes: - message (TEncodable): Incoming message. - channel (TEncodable): Name of an channel that triggered the message. - pattern (Optional[TEncodable]): Pattern that triggered the message. - """ - - message: TEncodable - channel: TEncodable - pattern: Optional[TEncodable] - async def get_pubsub_message(self) -> PubSubMsg: """ Returns the next pubsub message. diff --git a/python/python/glide/async_commands/standalone_commands.py b/python/glide/glide_async/python/glide/async_commands/standalone_commands.py similarity index 98% rename from python/python/glide/async_commands/standalone_commands.py rename to python/glide/glide_async/python/glide/async_commands/standalone_commands.py index 7413f14831..c68c284d66 100644 --- a/python/python/glide/async_commands/standalone_commands.py +++ b/python/glide/glide_async/python/glide/async_commands/standalone_commands.py @@ -4,24 +4,20 @@ from typing import Dict, List, Mapping, Optional, Union, cast -from glide.async_commands.batch import Batch -from glide.async_commands.command_args import ObjectType -from glide.async_commands.core import ( - CoreCommands, - FlushMode, - FunctionRestorePolicy, - InfoSection, -) -from glide.constants import ( +from ..async_commands import CoreCommands +from glide.shared.commands.batch import Batch +from glide.shared.commands.command_args import ObjectType +from glide.shared.commands.core_options import FlushMode, FunctionRestorePolicy, InfoSection +from glide.shared.constants import ( TOK, TEncodable, TFunctionListResponse, TFunctionStatsFullResponse, TResult, ) -from glide.protobuf.command_request_pb2 import RequestType +from glide.shared.protobuf.command_request_pb2 import RequestType -from ..glide import Script +from glide.glide_async.python.glide.glide import Script class StandaloneCommands(CoreCommands): diff --git a/python/python/glide/glide.pyi b/python/glide/glide_async/python/glide/glide.pyi similarity index 96% rename from python/python/glide/glide.pyi rename to python/glide/glide_async/python/glide/glide.pyi index bbd5274770..61d3469930 100644 --- a/python/python/glide/glide.pyi +++ b/python/glide/glide_async/python/glide/glide.pyi @@ -2,7 +2,7 @@ from collections.abc import Callable from enum import Enum from typing import List, Optional, Union -from glide.constants import TResult +from shared.constants import TResult DEFAULT_TIMEOUT_IN_MILLISECONDS: int = ... MAX_REQUEST_ARGS_LEN: int = ... diff --git a/python/python/glide/glide_client.py b/python/glide/glide_async/python/glide/glide_client.py similarity index 93% rename from python/python/glide/glide_client.py rename to python/glide/glide_async/python/glide/glide_client.py index 0fa53cef6e..251aa190e0 100644 --- a/python/python/glide/glide_client.py +++ b/python/glide/glide_async/python/glide/glide_client.py @@ -5,27 +5,29 @@ import threading from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast -from glide.async_commands.cluster_commands import ClusterCommands -from glide.async_commands.command_args import ObjectType -from glide.async_commands.core import CoreCommands -from glide.async_commands.standalone_commands import StandaloneCommands -from glide.config import BaseClientConfiguration, ServerCredentials -from glide.constants import DEFAULT_READ_BYTES_SIZE, OK, TEncodable, TRequest, TResult -from glide.exceptions import ( +from .async_commands.cluster_commands import ClusterCommands +from .async_commands.core import CoreCommands +from .async_commands.standalone_commands import StandaloneCommands +from glide.shared.commands.command_args import ObjectType +from glide.shared.commands.core_options import PubSubMsg +from glide.shared.config import BaseClientConfiguration, ServerCredentials +from glide.shared.constants import DEFAULT_READ_BYTES_SIZE, OK, TEncodable, TRequest, TResult +from glide.shared.exceptions import ( ClosingError, ConfigurationError, ConnectionError, ExecAbortError, RequestError, TimeoutError, + get_request_error_class, ) -from glide.logger import Level as LogLevel -from glide.logger import Logger as ClientLogger -from glide.protobuf.command_request_pb2 import Command, CommandRequest, RequestType -from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.protobuf.response_pb2 import RequestErrorType, Response -from glide.protobuf_codec import PartialMessageException, ProtobufCodec -from glide.routes import Route, set_protobuf_route +from glide.glide_async.python.glide.logger import Level as LogLevel +from glide.glide_async.python.glide.logger import Logger as ClientLogger +from glide.shared.protobuf.command_request_pb2 import Command, CommandRequest, RequestType +from glide.shared.protobuf.connection_request_pb2 import ConnectionRequest +from glide.shared.protobuf.response_pb2 import RequestErrorType, Response +from glide.shared.protobuf_codec import PartialMessageException, ProtobufCodec +from glide.shared.routes import Route, set_protobuf_route from .glide import ( DEFAULT_TIMEOUT_IN_MILLISECONDS, @@ -45,20 +47,6 @@ from typing_extensions import Self -def get_request_error_class( - error_type: Optional[RequestErrorType.ValueType], -) -> Type[RequestError]: - if error_type == RequestErrorType.Disconnect: - return ConnectionError - if error_type == RequestErrorType.ExecAbort: - return ExecAbortError - if error_type == RequestErrorType.Timeout: - return TimeoutError - if error_type == RequestErrorType.Unspecified: - return RequestError - return RequestError - - class BaseClient(CoreCommands): def __init__(self, config: BaseClientConfiguration): """ @@ -89,7 +77,7 @@ async def create(cls, config: BaseClientConfiguration) -> Self: Examples: # Connecting to a Standalone Server - >>> from glide import GlideClientConfiguration, NodeAddress, GlideClient, ServerCredentials, BackoffStrategy + >>> from shared import GlideClientConfiguration, NodeAddress, GlideClient, ServerCredentials, BackoffStrategy >>> config = GlideClientConfiguration( ... [ ... NodeAddress('primary.example.com', 6379), @@ -107,7 +95,7 @@ async def create(cls, config: BaseClientConfiguration) -> Self: >>> client = await GlideClient.create(config) # Connecting to a Cluster - >>> from glide import GlideClusterClientConfiguration, NodeAddress, GlideClusterClient, + >>> from shared import GlideClusterClientConfiguration, NodeAddress, GlideClusterClient, ... PeriodicChecksManualInterval >>> config = GlideClusterClientConfiguration( ... [ @@ -400,7 +388,7 @@ async def _execute_script( set_protobuf_route(request, route) return await self._write_request_await_response(request) - async def get_pubsub_message(self) -> CoreCommands.PubSubMsg: + async def get_pubsub_message(self) -> PubSubMsg: if self._is_closed: raise ClosingError( "Unable to execute requests; the client is closed. Please create a new client." @@ -426,7 +414,7 @@ async def get_pubsub_message(self) -> CoreCommands.PubSubMsg: self._pubsub_lock.release() return await response_future - def try_get_pubsub_message(self) -> Optional[CoreCommands.PubSubMsg]: + def try_get_pubsub_message(self) -> Optional[PubSubMsg]: if self._is_closed: raise ClosingError( "Unable to execute requests; the client is closed. Please create a new client." @@ -443,7 +431,7 @@ def try_get_pubsub_message(self) -> Optional[CoreCommands.PubSubMsg]: ) # locking might not be required - msg: Optional[CoreCommands.PubSubMsg] = None + msg: Optional[PubSubMsg] = None try: self._pubsub_lock.acquire() self._complete_pubsub_futures_safe() @@ -462,7 +450,7 @@ def _cancel_pubsub_futures_with_exception_safe(self, exception: ConnectionError) def _notification_to_pubsub_message_safe( self, response: Response - ) -> Optional[CoreCommands.PubSubMsg]: + ) -> Optional[PubSubMsg]: pubsub_message = None push_notification = cast( Dict[str, Any], value_from_pointer(response.resp_pointer) @@ -481,11 +469,11 @@ def _notification_to_pubsub_message_safe( ): values: List = push_notification["values"] if message_kind == "PMessage": - pubsub_message = BaseClient.PubSubMsg( + pubsub_message = PubSubMsg( message=values[2], channel=values[1], pattern=values[0] ) else: - pubsub_message = BaseClient.PubSubMsg( + pubsub_message = PubSubMsg( message=values[1], channel=values[0], pattern=None ) elif ( diff --git a/python/python/glide/logger.py b/python/glide/glide_async/python/glide/logger.py similarity index 100% rename from python/python/glide/logger.py rename to python/glide/glide_async/python/glide/logger.py diff --git a/python/src/lib.rs b/python/glide/glide_async/src/lib.rs similarity index 100% rename from python/src/lib.rs rename to python/glide/glide_async/src/lib.rs diff --git a/python/glide/glide_sync/glide_sync/__init__.py b/python/glide/glide_sync/glide_sync/__init__.py new file mode 100644 index 0000000000..2e0e61be00 --- /dev/null +++ b/python/glide/glide_sync/glide_sync/__init__.py @@ -0,0 +1,5 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from .glide_client import BaseClient, GlideClient, GlideClusterClient, TGlideClient + +__all__ = ["BaseClient", "GlideClient", "GlideClusterClient", "TGlideClient"] diff --git a/python/glide/glide_sync/glide_sync/glide_client.py b/python/glide/glide_sync/glide_sync/glide_client.py new file mode 100644 index 0000000000..5e47efbc2b --- /dev/null +++ b/python/glide/glide_sync/glide_sync/glide_client.py @@ -0,0 +1,377 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +import sys +from pathlib import Path +from typing import List, Optional, Union + +from cffi import FFI +from .sync_commands.cluster_commands import ClusterCommands +from .sync_commands.core import CoreCommands +from .sync_commands.standalone_commands import StandaloneCommands +from glide.shared.config import BaseClientConfiguration, GlideClusterClientConfiguration +from glide.shared.constants import OK, TEncodable, TResult +from glide.shared.exceptions import ClosingError, RequestError, get_request_error_class +from shared.protobuf.command_request_pb2 import RequestType +from glide.shared.routes import Route + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +ENCODING = "utf-8" +CURR_DIR = Path(__file__).resolve().parent +ROOT_DIR = CURR_DIR.parent.parent.parent.parent +FFI_DIR = ROOT_DIR / "ffi" +LIB_FILE = FFI_DIR / "target" / "debug" / "libglide_ffi.so" + + +# Enum values must match the Rust definition +class FFIClientTypeEnum: + Async = 0 + Sync = 1 + + +class BaseClient(CoreCommands): + + def __init__(self, config: BaseClientConfiguration): + """ + To create a new client, use the `create` classmethod + """ + self.config: BaseClientConfiguration = config + self._is_closed: bool = False + + @classmethod + def create(cls, config: BaseClientConfiguration) -> Self: + self = cls(config) + self._init_ffi() + self.config = config + self._is_closed = False + conn_req = config._create_a_protobuf_conn_request( + cluster_mode=type(config) is GlideClusterClientConfiguration + ) + conn_req_bytes = conn_req.SerializeToString() + client_type = self.ffi.new( + "ClientType*", + { + "_type": self.ffi.cast("ClientTypeEnum", FFIClientTypeEnum.Sync), + }, + ) + client_response_ptr = self.lib.create_client( + conn_req_bytes, len(conn_req_bytes), client_type + ) + # Handle the connection response + if client_response_ptr != self.ffi.NULL: + client_response = self._try_ffi_cast( + "ConnectionResponse*", client_response_ptr + ) + if client_response.conn_ptr != self.ffi.NULL: + self.core_client = client_response.conn_ptr + else: + error_message = ( + self.ffi.string(client_response.connection_error_message).decode( + ENCODING + ) + if client_response.connection_error_message != self.ffi.NULL + else "Unknown error" + ) + raise ClosingError(error_message) + + # Free the connection response to avoid memory leaks + self.lib.free_connection_response(client_response_ptr) + else: + raise ClosingError("Failed to create client, response pointer is NULL.") + return self + + def _init_ffi(self): + self.ffi = FFI() + + # Define the CommandResponse struct and related types + self.ffi.cdef( + """ + struct CommandResponse { + int response_type; + long int_value; + double float_value; + bool bool_value; + char* string_value; + long string_value_len; + struct CommandResponse* array_value; + long array_value_len; + struct CommandResponse* map_key; + struct CommandResponse* map_value; + struct CommandResponse* sets_value; + long sets_value_len; + }; + + typedef struct CommandResponse CommandResponse; + + typedef enum { + Null = 0, + Int = 1, + Float = 2, + Bool = 3, + String = 4, + Array = 5, + Map = 6, + Sets = 7 + } ResponseType; + + typedef void (*SuccessCallback)(uintptr_t index_ptr, const CommandResponse* message); + typedef void (*FailureCallback)(uintptr_t index_ptr, const char* error_message, int error_type); + + typedef struct { + const void* conn_ptr; + const char* connection_error_message; + } ConnectionResponse; + + typedef struct { + const char* command_error_message; + int command_error_type; + } CommandError; + + typedef struct { + CommandResponse* response; + CommandError* command_error; + } CommandResult; + + typedef enum { + Async = 0, + Sync = 1 + } ClientTypeEnum; + + typedef struct { + SuccessCallback success_callback; + FailureCallback failure_callback; + } AsyncClient; + + typedef struct { + int _type; // Enum to differentiate between Async and Sync + union { + struct { + void (*success_callback)(uintptr_t, const void*); + void (*failure_callback)(uintptr_t, const char*, int); + } async_client; + }; + } ClientType; + + // Function declarations + const ConnectionResponse* create_client( + const uint8_t* connection_request_bytes, + size_t connection_request_len, + const ClientType* client_type // Pass by pointer + ); + void close_client(const void* client_adapter_ptr); + void free_connection_response(ConnectionResponse* connection_response_ptr); + char* get_response_type_string(int response_type); + void free_response_type_string(char* response_string); + void free_command_response(CommandResponse* command_response_ptr); + void free_error_message(char* error_message); + void free_command_result(CommandResult* command_result_ptr); + CommandResult* command( + const void* client_adapter_ptr, uintptr_t channel, int command_type, + unsigned long arg_count, const size_t *args, const unsigned long* args_len, + const unsigned char* route_bytes, size_t route_bytes_len + ); + + """ + ) + + # Load the shared library (adjust the path to your compiled Rust library) + self.lib = self.ffi.dlopen(str(LIB_FILE.resolve())) + + def _handle_response(self, message): + if message == self.ffi.NULL: + raise RequestError("Received NULL message.") + + message_type = self.ffi.typeof(message).cname + if message_type == "CommandResponse *": + message = message[0] + message_type = self.ffi.typeof(message).cname + + if message_type != "CommandResponse": + raise RequestError(f"Unexpected message type = {message_type}") + + return self._handle_command_response(message) + + def _handle_command_response(self, msg): + """Handle a CommandResponse message based on its response type.""" + handlers = { + 0: self._handle_null_response, + 1: self._handle_int_response, + 2: self._handle_float_response, + 3: self._handle_bool_response, + 4: self._handle_string_response, + 5: self._handle_array_response, + 6: self._handle_map_response, + 7: self._handle_set_response, + 8: self._handle_ok_response, + } + + handler = handlers.get(msg.response_type) + if handler is None: + raise RequestError(f"Unhandled response type = {msg.response_type}") + + return handler(msg) + + def _handle_null_response(self, msg): + return None + + def _handle_int_response(self, msg): + return msg.int_value + + def _handle_float_response(self, msg): + return msg.float_value + + def _handle_bool_response(self, msg): + return bool(msg.bool_value) + + def _handle_string_response(self, msg): + try: + return self.ffi.buffer(msg.string_value, msg.string_value_len)[:] + except Exception as e: + raise RequestError(f"Error decoding string value: {e}") + + def _handle_array_response(self, msg): + array = [] + for i in range(msg.array_value_len): + element = self._try_ffi_cast("struct CommandResponse*", msg.array_value + i) + array.append(self._handle_response(element)) + return array + + def _handle_map_response(self, msg): + map_dict = {} + for i in range(msg.array_value_len): + element = self._try_ffi_cast("struct CommandResponse*", msg.array_value + i) + key = self._try_ffi_cast("struct CommandResponse*", element.map_key) + value = self._try_ffi_cast("struct CommandResponse*", element.map_value) + map_dict[self._handle_response(key)] = self._handle_response(value) + return map_dict + + def _handle_set_response(self, msg): + result_set = set() + sets_array = self._try_ffi_cast( + f"struct CommandResponse[{msg.sets_value_len}]", msg.sets_value + ) + for i in range(msg.sets_value_len): + element = sets_array[i] + result_set.add(self._handle_response(element)) + return result_set + + def _handle_ok_response(self, msg): + return OK + + def _try_ffi_cast(self, type, source): + try: + return self.ffi.cast(type, source) + except Exception as e: + raise ClosingError(f"FFI casting failed: {e}") + + def _to_c_strings(self, args): + """Convert Python arguments to C-compatible pointers and lengths.""" + c_strings = [] + string_lengths = [] + buffers = [] # Keep a reference to prevent premature garbage collection + + for arg in args: + if isinstance(arg, str): + # Convert string to bytes + arg_bytes = arg.encode(ENCODING) + elif isinstance(arg, (int, float)): + # Convert numeric values to strings and then to bytes + arg_bytes = str(arg).encode(ENCODING) + elif isinstance(arg, bytes): + arg_bytes = arg + else: + raise ValueError(f"Unsupported argument type: {type(arg)}") + + # Use ffi.from_buffer for zero-copy conversion + buffers.append(arg_bytes) # Keep the byte buffer alive + c_strings.append( + self._try_ffi_cast("size_t", self.ffi.from_buffer(arg_bytes)) + ) + string_lengths.append(len(arg_bytes)) + # Return C-compatible arrays and keep buffers alive + return ( + self.ffi.new("size_t[]", c_strings), + self.ffi.new("unsigned long[]", string_lengths), + buffers, # Ensure buffers stay alive + ) + + def _handle_cmd_result(self, command_result): + try: + if command_result == self.ffi.NULL: + raise ClosingError("Internal error: Received NULL as a command result") + if command_result.command_error != self.ffi.NULL: + # Handle the error case + error = self._try_ffi_cast( + "CommandError*", command_result.command_error + ) + error_message = self.ffi.string(error.command_error_message).decode( + ENCODING + ) + error_class = get_request_error_class(error.command_error_type) + # Free the error message to avoid memory leaks + raise error_class(error_message) + else: + return self._handle_response(command_result.response) + # Free the error message to avoid memory leaks + finally: + self.lib.free_command_result(command_result) + + def _execute_command( + self, + request_type: RequestType.ValueType, + args: List[TEncodable], + route: Optional[Route] = None, + ) -> TResult: + if self._is_closed: + raise ClosingError( + "Unable to execute requests; the client is closed. Please create a new client." + ) + client_adapter_ptr = self.core_client + if client_adapter_ptr == self.ffi.NULL: + raise ValueError("Invalid client pointer.") + + # Convert the arguments to C-compatible pointers + c_args, c_lengths, buffers = self._to_c_strings(args) + # Call the command function + route_bytes = b"" # TODO: add support for route + route_ptr = self.ffi.new("unsigned char[]", route_bytes) + + result = self.lib.command( + client_adapter_ptr, # Client pointer + 1, # Example channel (adjust as needed) + request_type, # Request type (e.g., GET or SET) + len(args), # Number of arguments + c_args, # Array of argument pointers + c_lengths, # Array of argument lengths + route_ptr, + len(route_bytes), + ) + return self._handle_cmd_result(result) + + def close(self): + if not self._is_closed: + self.lib.close_client(self.core_client) + self.core_client = self.ffi.NULL + self._is_closed = True + + +class GlideClusterClient(BaseClient, ClusterCommands): + """ + Client used for connection to cluster servers. + For full documentation, see + https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#cluster + """ + + +class GlideClient(BaseClient, StandaloneCommands): + """ + Client used for connection to standalone servers. + For full documentation, see + https://github.com/valkey-io/valkey-glide/wiki/Python-wrapper#standalone + """ + + +TGlideClient = Union[GlideClient, GlideClusterClient] diff --git a/python/glide/glide_sync/glide_sync/sync_commands/__init__.py b/python/glide/glide_sync/glide_sync/sync_commands/__init__.py new file mode 100644 index 0000000000..8aaf21baff --- /dev/null +++ b/python/glide/glide_sync/glide_sync/sync_commands/__init__.py @@ -0,0 +1,5 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + +from .core import CoreCommands + +__all__ = ["CoreCommands"] diff --git a/python/glide/glide_sync/glide_sync/sync_commands/cluster_commands.py b/python/glide/glide_sync/glide_sync/sync_commands/cluster_commands.py new file mode 100644 index 0000000000..93b7bf90c3 --- /dev/null +++ b/python/glide/glide_sync/glide_sync/sync_commands/cluster_commands.py @@ -0,0 +1,65 @@ +from typing import List, Optional, cast + +from glide.shared.commands.core_options import InfoSection +from .core import CoreCommands +from glide.shared.constants import TClusterResponse, TEncodable, TResult +from glide.shared.protobuf.command_request_pb2 import RequestType +from glide.shared.routes import Route + + +class ClusterCommands(CoreCommands): + def custom_command( + self, command_args: List[TEncodable], route: Optional[Route] = None + ) -> TClusterResponse[TResult]: + """ + Executes a single command, without checking inputs. + See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + for details on the restrictions and limitations of the custom command API. + + For example - Return a list of all pub/sub clients from all nodes:: + + connection.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"], AllNodes()) + + Args: + command_args (List[TEncodable]): List of the command's arguments, where each argument is either a string or bytes. + Every part of the command, including the command name and subcommands, should be added as a separate value in args. + route (Optional[Route]): The command will be routed automatically based on the passed command's default request + policy, unless `route` is provided, in which + case the client will route the command to the nodes defined by `route`. Defaults to None. + + Returns: + TClusterResponse[TResult]: The returning value depends on the executed command and the route. + """ + return cast( + TClusterResponse[TResult], + self._execute_command(RequestType.CustomCommand, command_args, route), + ) + + def info( + self, + sections: Optional[List[InfoSection]] = None, + route: Optional[Route] = None, + ) -> TClusterResponse[bytes]: + """ + Get information and statistics about the server. + + See [valkey.io](https://valkey.io/commands/info/) for details. + + Args: + sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of + information to retrieve. When no parameter is provided, the default option is assumed. + route (Optional[Route]): The command will be routed to all primaries, unless `route` is provided, in which + case the client will route the command to the nodes defined by `route`. Defaults to None. + + Returns: + TClusterResponse[bytes]: If a single node route is requested, returns a bytes string containing the information for + the required sections. Otherwise, returns a dict of bytes strings, with each key containing the address of + the queried node and value containing the information regarding the requested sections. + """ + args: List[TEncodable] = ( + [section.value for section in sections] if sections else [] + ) + return cast( + TClusterResponse[bytes], + self._execute_command(RequestType.Info, args, route), + ) diff --git a/python/glide/glide_sync/glide_sync/sync_commands/core.py b/python/glide/glide_sync/glide_sync/sync_commands/core.py new file mode 100644 index 0000000000..85bc0eaaa9 --- /dev/null +++ b/python/glide/glide_sync/glide_sync/sync_commands/core.py @@ -0,0 +1,159 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from typing import List, Optional, Protocol, Tuple, cast + +from glide.shared.commands.command_args import ObjectType +from glide.shared.commands.core_options import ConditionalChange, ExpirySet +from glide.shared.constants import TOK, TEncodable, TResult +from glide.shared.protobuf.command_request_pb2 import RequestType +from glide.shared.routes import Route + + +class CoreCommands(Protocol): + def _execute_command( + self, + request_type: RequestType.ValueType, + args: List[TEncodable], + route: Optional[Route] = ..., + ) -> TResult: ... + + def _execute_transaction( + self, + commands: List[Tuple[RequestType.ValueType, List[TEncodable]]], + route: Optional[Route] = None, + ) -> List[TResult]: ... + + def _execute_script( + self, + hash: str, + keys: Optional[List[TEncodable]] = None, + args: Optional[List[TEncodable]] = None, + route: Optional[Route] = None, + ) -> TResult: ... + + def _update_connection_password( + self, password: Optional[str], immediate_auth: bool + ) -> TResult: ... + + def update_connection_password( + self, password: Optional[str], immediate_auth=False + ) -> TOK: + """ + Update the current connection password with a new password. + + **Note:** This method updates the client's internal password configuration and does + not perform password rotation on the server side. + + This method is useful in scenarios where the server password has changed or when + utilizing short-lived passwords for enhanced security. It allows the client to + update its password to reconnect upon disconnection without the need to recreate + the client instance. This ensures that the internal reconnection mechanism can + handle reconnection seamlessly, preventing the loss of in-flight commands. + + Args: + password (`Optional[str]`): The new password to use for the connection, + if `None` the password will be removed. + immediate_auth (`bool`): + `True`: The client will authenticate immediately with the new password against all connections, Using `AUTH` + command. If password supplied is an empty string, auth will not be performed and warning will be returned. + The default is `False`. + + Returns: + TOK: A simple OK response. If `immediate_auth=True` returns OK if the reauthenticate succeed. + + Example: + >>> client.update_connection_password("new_password", immediate_auth=True) + 'OK' + """ + return cast(TOK, self._update_connection_password(password, immediate_auth)) + + def set( + self, + key: TEncodable, + value: TEncodable, + conditional_set: Optional[ConditionalChange] = None, + expiry: Optional[ExpirySet] = None, + return_old_value: bool = False, + ) -> Optional[bytes]: + """ + Set the given key with the given value. Return value is dependent on the passed options. + + See [valkey.io](https://valkey.io/commands/set/) for more details. + + Args: + key (TEncodable): the key to store. + value (TEncodable): the value to store with the given key. + conditional_set (Optional[ConditionalChange], optional): set the key only if the given condition is met. + Equivalent to [`XX` | `NX` | `IFEQ` comparison-value] in the Valkey API. Defaults to None. + expiry (Optional[ExpirySet], optional): set expiriation to the given key. + Equivalent to [`EX` | `PX` | `EXAT` | `PXAT` | `KEEPTTL`] in the Valkey API. Defaults to None. + return_old_value (bool, optional): Return the old value stored at key, or None if key did not exist. + An error is returned and SET aborted if the value stored at key is not a string. + Equivalent to `GET` in the Valkey API. Defaults to False. + + Returns: + Optional[bytes]: If the value is successfully set, return OK. + + If value isn't set because of `only_if_exists` or `only_if_does_not_exist` conditions, return `None`. + + If return_old_value is set, return the old value as a bytes string. + + Example: + >>> client.set(b"key", b"value") + 'OK' + # ONLY_IF_EXISTS -> Only set the key if it already exists + # expiry -> Set the amount of time until key expires + >>> client.set( + ... "key", + ... "new_value", + ... conditional_set=ConditionalChange.ONLY_IF_EXISTS, + ... expiry=ExpirySet(ExpiryType.SEC, 5) + ... ) + 'OK' # Set "new_value" to "key" only if "key" already exists, and set the key expiration to 5 seconds. + # ONLY_IF_DOES_NOT_EXIST -> Only set key if it does not already exist + >>> client.set( + ... "key", + ... "value", + ... conditional_set=ConditionalChange.ONLY_IF_DOES_NOT_EXIST, + ... return_old_value=True + ... ) + b'new_value' # Returns the old value of "key". + >>> client.get("key") + b'new_value' # Value wasn't modified back to being "value" because of "NX" flag. + # ONLY_IF_EQUAL -> Only set key if provided value is equal to current value of the key + >>> client.set("key", "value") + 'OK' # Reset "key" to "value" + >>> client.set("key", "new_value", conditional_set=OnlyIfEqual("different_value")) + 'None' # Did not rewrite value of "key" because provided value was not equal to the previous value of "key" + >>> client.get("key") + b'value' # Still the original value because nothing got rewritten in the last call + >>> client.set("key", "new_value", conditional_set=OnlyIfEqual("value")) + 'OK' + >>> client.get("key") + b'newest_value' # Set "key" to "new_value" because the provided value was equal to the previous value of "key" + """ + args = [key, value] + if conditional_set: + args.append(conditional_set.value) + if return_old_value: + args.append("GET") + if expiry is not None: + args.extend(expiry.get_cmd_args()) + return cast(Optional[bytes], self._execute_command(RequestType.Set, args)) + + def get(self, key: TEncodable) -> Optional[bytes]: + """ + Get the value associated with the given key, or null if no such value exists. + See https://valkey.io/commands/get/ for details. + + Args: + key (TEncodable): The key to retrieve from the database. + + Returns: + Optional[bytes]: If the key exists, returns the value of the key as a byte string. Otherwise, return None. + + Example: + >>> client.get("key") + b'value' + """ + args: List[TEncodable] = [key] + return cast(Optional[bytes], self._execute_command(RequestType.Get, args)) diff --git a/python/glide/glide_sync/glide_sync/sync_commands/standalone_commands.py b/python/glide/glide_sync/glide_sync/sync_commands/standalone_commands.py new file mode 100644 index 0000000000..6993628005 --- /dev/null +++ b/python/glide/glide_sync/glide_sync/sync_commands/standalone_commands.py @@ -0,0 +1,44 @@ +from typing import List, Optional, cast + +from glide.shared.commands.core_options import InfoSection +from .core import CoreCommands +from glide.shared.constants import TEncodable, TResult +from .protobuf.command_request_pb2 import RequestType + + +class StandaloneCommands(CoreCommands): + def custom_command(self, command_args: List[TEncodable]) -> TResult: + """ + Executes a single command, without checking inputs. + See the [Valkey GLIDE Wiki](https://github.com/valkey-io/valkey-glide/wiki/General-Concepts#custom-command) + for details on the restrictions and limitations of the custom command API. + Args: + command_args (List[TEncodable]): List of the command's arguments, where each argument is either a string or bytes. + Every part of the command, including the command name and subcommands, should be added as a separate value in args. + Returns: + TResult: The returning value depends on the executed command. + Example: + >>> connection.customCommand(["CLIENT", "LIST","TYPE", "PUBSUB"]) + """ + return self._execute_command(RequestType.CustomCommand, command_args) + + def info( + self, + sections: Optional[List[InfoSection]] = None, + ) -> bytes: + """ + Get information and statistics about the server. + + See [valkey.io](https://valkey.io/commands/info/) for details. + + Args: + sections (Optional[List[InfoSection]]): A list of InfoSection values specifying which sections of + information to retrieve. When no parameter is provided, the default option is assumed. + + Returns: + bytes: Returns bytes containing the information for the sections requested. + """ + args: List[TEncodable] = ( + [section.value for section in sections] if sections else [] + ) + return cast(bytes, self._execute_command(RequestType.Info, args)) diff --git a/python/glide/glide_sync/pyproject.toml b/python/glide/glide_sync/pyproject.toml new file mode 100644 index 0000000000..19feb54368 --- /dev/null +++ b/python/glide/glide_sync/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel", "cffi>=1.0.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "glide_sync" +version = "0.1.0" +description = "Valkey GLIDE Sync client (CFFI-based)" +requires-python = ">=3.9" +dependencies = [ + "cffi>=1.0.0", + "protobuf>=3.20", +] + +[tool.setuptools] +packages = ["glide_sync", "glide_sync.sync_commands", "shared", "shared.commands"] +include-package-data = true + +[tool.setuptools.package-data] +glide_sync = ["*.so", "*.pyd", "*.dll", "*.pyi", "py.typed"] diff --git a/python/python/glide/py.typed b/python/glide/py.typed similarity index 100% rename from python/python/glide/py.typed rename to python/glide/py.typed diff --git a/python/python/glide/__init__.py b/python/glide/shared/__init__.py similarity index 69% rename from python/python/glide/__init__.py rename to python/glide/shared/__init__.py index 7b76b046c4..d739f912b0 100644 --- a/python/python/glide/__init__.py +++ b/python/glide/shared/__init__.py @@ -1,13 +1,13 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from glide.async_commands.batch import ( +from .commands.batch import ( Batch, ClusterBatch, ClusterTransaction, TBatch, Transaction, ) -from glide.async_commands.bitmap import ( +from .commands.bitmap import ( BitEncoding, BitFieldGet, BitFieldIncrBy, @@ -24,10 +24,9 @@ SignedEncoding, UnsignedEncoding, ) -from glide.async_commands.command_args import Limit, ListDirection, ObjectType, OrderBy -from glide.async_commands.core import ( +from .commands.command_args import Limit, ListDirection, ObjectType, OrderBy +from .commands.core_options import ( ConditionalChange, - CoreCommands, ExpireOptions, ExpiryGetEx, ExpirySet, @@ -38,10 +37,11 @@ InfoSection, InsertPosition, OnlyIfEqual, + PubSubMsg, UpdateOptions, ) -from glide.async_commands.server_modules import ft, glide_json, json_batch -from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( +from .commands.server_modules import ft, glide_json, json_batch +from .commands.server_modules.ft_options.ft_aggregate_options import ( FtAggregateApply, FtAggregateClause, FtAggregateFilter, @@ -52,7 +52,7 @@ FtAggregateSortBy, FtAggregateSortProperty, ) -from glide.async_commands.server_modules.ft_options.ft_create_options import ( +from .commands.server_modules.ft_options.ft_create_options import ( DataType, DistanceMetricType, Field, @@ -68,21 +68,21 @@ VectorFieldAttributesHnsw, VectorType, ) -from glide.async_commands.server_modules.ft_options.ft_profile_options import ( +from .commands.server_modules.ft_options.ft_profile_options import ( FtProfileOptions, QueryType, ) -from glide.async_commands.server_modules.ft_options.ft_search_options import ( +from .commands.server_modules.ft_options.ft_search_options import ( FtSearchLimit, FtSearchOptions, ReturnField, ) -from glide.async_commands.server_modules.glide_json import ( +from .commands.server_modules.glide_json import ( JsonArrIndexOptions, JsonArrPopOptions, JsonGetOptions, ) -from glide.async_commands.sorted_set import ( +from .commands.sorted_set import ( AggregationType, GeoSearchByBox, GeoSearchByRadius, @@ -97,7 +97,7 @@ ScoreBoundary, ScoreFilter, ) -from glide.async_commands.stream import ( +from .commands.stream import ( ExclusiveIdBound, IdBound, MaxId, @@ -113,7 +113,7 @@ TrimByMaxLen, TrimByMinId, ) -from glide.config import ( +from glide.shared.config import ( AdvancedGlideClientConfiguration, AdvancedGlideClusterClientConfiguration, BackoffStrategy, @@ -126,7 +126,8 @@ ReadFrom, ServerCredentials, ) -from glide.constants import ( +from .protobuf.command_request_pb2 import RequestType +from glide.shared.constants import ( OK, TOK, FtAggregateResponse, @@ -145,7 +146,7 @@ TXInfoStreamFullResponse, TXInfoStreamResponse, ) -from glide.exceptions import ( +from glide.shared.exceptions import ( ClosingError, ConfigurationError, ConnectionError, @@ -154,10 +155,9 @@ RequestError, TimeoutError, ) -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient -from glide.logger import Level as LogLevel -from glide.logger import Logger -from glide.routes import ( +from glide.glide_async.python.glide.logger import Level as LogLevel +from glide.glide_async.python.glide.logger import Logger +from glide.shared.routes import ( AllNodes, AllPrimaries, ByAddressRoute, @@ -168,19 +168,54 @@ SlotType, ) -from .glide import ClusterScanCursor, Script +from glide.glide_async.python.glide.glide import ClusterScanCursor, Script -PubSubMsg = CoreCommands.PubSubMsg + +# try: +# from glide.glide_async.python.glide import TGlideClient +# from glide.glide_async.python.glide import GlideClient as AsyncGlideClient +# from glide.glide_async.python.glide import GlideClusterClient as AsyncGlideClusterClient +# GlideClient = AsyncGlideClient +# GlideClusterClient = AsyncGlideClusterClient +# except ImportError: +# try: +# from glide.glide_sync.glide_sync import TGlideClient +# from glide.glide_sync.glide_sync import GlideClient as SyncGlideClient +# from glide.glide_sync.glide_sync import GlideClusterClient as SyncGlideClusterClient +# GlideClient = SyncGlideClient +# GlideClusterClient = SyncGlideClusterClient +# except ImportError as e: +# raise ImportError( +# f"GlideClient not available — please install with either " +# "`valkey-glide[async]` or `valkey-glide[sync]`.\n{e}" +# ) + +# # Optional named exports if both are installed +# try: +# from glide.glide_sync.glide_sync import TGlideClient +# from glide.glide_sync.glide_sync import GlideClient as SyncGlideClient +# from glide.glide_sync.glide_sync import GlideClusterClient as SyncGlideClusterClient +# except ImportError: +# SyncGlideClient = None +# SyncGlideClusterClient = None + +# try: +# from glide.glide_async.python.glide import TGlideClient +# from glide.glide_async.python.glide import GlideClient as AsyncGlideClient +# from glide.glide_async.python.glide import GlideClusterClient as AsyncGlideClusterClient +# except ImportError: +# AsyncGlideClient = None +# AsyncGlideClusterClient = None __all__ = [ # Client - "GlideClient", - "GlideClusterClient", + # "GlideClient", + # "GlideClusterClient", "Batch", "ClusterBatch", "ClusterTransaction", "Transaction", - "TGlideClient", + # "TGlideClient", "TBatch", # Config "AdvancedGlideClientConfiguration", @@ -331,4 +366,6 @@ "FtAggregateSortProperty", "FtProfileOptions", "QueryType", + # protobuf + "RequestType", ] diff --git a/python/glide/shared/commands/__init__.py b/python/glide/shared/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/python/glide/async_commands/batch.py b/python/glide/shared/commands/batch.py similarity index 99% rename from python/python/glide/async_commands/batch.py rename to python/glide/shared/commands/batch.py index 77e5407644..2f75906952 100644 --- a/python/python/glide/async_commands/batch.py +++ b/python/glide/shared/commands/batch.py @@ -4,7 +4,7 @@ from typing import List, Mapping, Optional, Tuple, TypeVar, Union from deprecated import deprecated -from glide.async_commands.bitmap import ( +from ..commands.bitmap import ( BitFieldGet, BitFieldSubCommands, BitwiseOperation, @@ -12,26 +12,26 @@ _create_bitfield_args, _create_bitfield_read_only_args, ) -from glide.async_commands.command_args import Limit, ListDirection, OrderBy -from glide.async_commands.core import ( +from ..commands.command_args import Limit, ListDirection, OrderBy +from ..commands.core_options import ( ConditionalChange, ExpireOptions, ExpiryGetEx, ExpirySet, FlushMode, FunctionRestorePolicy, - GeospatialData, - GeoUnit, InfoSection, InsertPosition, UpdateOptions, _build_sort_args, ) -from glide.async_commands.sorted_set import ( +from ..commands.sorted_set import ( AggregationType, GeoSearchByBox, GeoSearchByRadius, GeoSearchCount, + GeospatialData, + GeoUnit, InfBound, LexBoundary, RangeByIndex, @@ -43,7 +43,7 @@ _create_zinter_zunion_cmd_args, _create_zrange_args, ) -from glide.async_commands.stream import ( +from ..commands.stream import ( StreamAddOptions, StreamClaimOptions, StreamGroupOptions, @@ -54,8 +54,8 @@ StreamTrimOptions, _create_xpending_range_args, ) -from glide.constants import TEncodable -from glide.protobuf.command_request_pb2 import RequestType +from glide.shared.constants import TEncodable +from ..protobuf.command_request_pb2 import RequestType TBatch = TypeVar("TBatch", bound="BaseBatch") diff --git a/python/python/glide/async_commands/bitmap.py b/python/glide/shared/commands/bitmap.py similarity index 100% rename from python/python/glide/async_commands/bitmap.py rename to python/glide/shared/commands/bitmap.py diff --git a/python/python/glide/async_commands/command_args.py b/python/glide/shared/commands/command_args.py similarity index 100% rename from python/python/glide/async_commands/command_args.py rename to python/glide/shared/commands/command_args.py diff --git a/python/glide/shared/commands/core_options.py b/python/glide/shared/commands/core_options.py new file mode 100644 index 0000000000..68765a888e --- /dev/null +++ b/python/glide/shared/commands/core_options.py @@ -0,0 +1,395 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import Enum +from typing import List, Optional, Type, Union, get_args + +from ..commands.command_args import Limit, OrderBy +from glide.shared.constants import TEncodable + + +@dataclass +class PubSubMsg: + """ + Describes the incoming pubsub message + + Attributes: + message (TEncodable): Incoming message. + channel (TEncodable): Name of an channel that triggered the message. + pattern (Optional[TEncodable]): Pattern that triggered the message. + """ + + message: TEncodable + channel: TEncodable + pattern: Optional[TEncodable] + + +class ConditionalChange(Enum): + """ + A condition to the `SET`, `ZADD` and `GEOADD` commands. + """ + + ONLY_IF_EXISTS = "XX" + """ Only update key / elements that already exist. Equivalent to `XX` in the Valkey API. """ + + ONLY_IF_DOES_NOT_EXIST = "NX" + """ Only set key / add elements that does not already exist. Equivalent to `NX` in the Valkey API. """ + + +@dataclass +class OnlyIfEqual: + """ + Change condition to the `SET` command, + For additional conditonal options see ConditionalChange + + - comparison_value - value to compare to the current value of a key. + + If comparison_value is equal to the key, it will overwrite the value of key to the new provided value + Equivalent to the IFEQ comparison-value in the Valkey API + """ + + comparison_value: TEncodable + + +class ExpiryType(Enum): + """ + SET option: The type of the expiry. + """ + + SEC = 0, Union[int, timedelta] + """ + Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. + """ + + MILLSEC = 1, Union[int, timedelta] + """ + Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. + """ + + UNIX_SEC = 2, Union[int, datetime] + """ + Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. + """ + + UNIX_MILLSEC = 3, Union[int, datetime] + """ + Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Valkey API. + """ + + KEEP_TTL = 4, Type[None] + """ + Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Valkey API. + """ + + +class ExpiryTypeGetEx(Enum): + """ + GetEx option: The type of the expiry. + """ + + SEC = 0, Union[int, timedelta] + """ Set the specified expire time, in seconds. Equivalent to `EX` in the Valkey API. """ + + MILLSEC = 1, Union[int, timedelta] + """ Set the specified expire time, in milliseconds. Equivalent to `PX` in the Valkey API. """ + + UNIX_SEC = 2, Union[int, datetime] + """ Set the specified Unix time at which the key will expire, in seconds. Equivalent to `EXAT` in the Valkey API. """ + + UNIX_MILLSEC = 3, Union[int, datetime] + """ Set the specified Unix time at which the key will expire, in milliseconds. Equivalent to `PXAT` in the Valkey API. """ + + PERSIST = 4, Type[None] + """ Remove the time to live associated with the key. Equivalent to `PERSIST` in the Valkey API. """ + + +class InfoSection(Enum): + """ + INFO option: a specific section of information: + + When no parameter is provided, the default option is assumed. + """ + + SERVER = "server" + """ General information about the server """ + + CLIENTS = "clients" + """ Client connections section """ + + MEMORY = "memory" + """ Memory consumption related information """ + + PERSISTENCE = "persistence" + """ RDB and AOF related information """ + + STATS = "stats" + """ General statistics """ + + REPLICATION = "replication" + """ Master/replica replication information """ + + CPU = "cpu" + """ CPU consumption statistics """ + + COMMAND_STATS = "commandstats" + """ Valkey command statistics """ + + LATENCY_STATS = "latencystats" + """ Valkey command latency percentile distribution statistics """ + + SENTINEL = "sentinel" + """ Valkey Sentinel section (only applicable to Sentinel instances) """ + + CLUSTER = "cluster" + """ Valkey Cluster section """ + + MODULES = "modules" + """ Modules section """ + + KEYSPACE = "keyspace" + """ Database related statistics """ + + ERROR_STATS = "errorstats" + """ Valkey error statistics """ + + ALL = "all" + """ Return all sections (excluding module generated ones) """ + + DEFAULT = "default" + """ Return only the default set of sections """ + + EVERYTHING = "everything" + """ Includes all and modules """ + + +class ExpireOptions(Enum): + """ + EXPIRE option: options for setting key expiry. + """ + + HasNoExpiry = "NX" + """ Set expiry only when the key has no expiry (Equivalent to "NX" in Valkey). """ + + HasExistingExpiry = "XX" + """ Set expiry only when the key has an existing expiry (Equivalent to "XX" in Valkey). """ + + NewExpiryGreaterThanCurrent = "GT" + """ + Set expiry only when the new expiry is greater than the current one (Equivalent to "GT" in Valkey). + """ + + NewExpiryLessThanCurrent = "LT" + """ + Set expiry only when the new expiry is less than the current one (Equivalent to "LT" in Valkey). + """ + + +class UpdateOptions(Enum): + """ + Options for updating elements of a sorted set key. + """ + + LESS_THAN = "LT" + """ Only update existing elements if the new score is less than the current score. """ + + GREATER_THAN = "GT" + """ Only update existing elements if the new score is greater than the current score. """ + + +class ExpirySet: + """ + SET option: Represents the expiry type and value to be executed with "SET" command. + + Attributes: + cmd_arg (str): The expiry type. + value (str): The value for the expiry type. + """ + + def __init__( + self, + expiry_type: ExpiryType, + value: Optional[Union[int, datetime, timedelta]], + ) -> None: + self.set_expiry_type_and_value(expiry_type, value) + + def __eq__(self, other: "object") -> bool: + if not isinstance(other, ExpirySet): + return NotImplemented + return self.expiry_type == other.expiry_type and self.value == other.value + + def set_expiry_type_and_value( + self, expiry_type: ExpiryType, value: Optional[Union[int, datetime, timedelta]] + ): + """ + Args: + expiry_type (ExpiryType): The expiry type. + value (Optional[Union[int, datetime, timedelta]]): The value of the expiration type. The type of expiration + determines the type of expiration value: + + - SEC: Union[int, timedelta] + - MILLSEC: Union[int, timedelta] + - UNIX_SEC: Union[int, datetime] + - UNIX_MILLSEC: Union[int, datetime] + - KEEP_TTL: Type[None] + """ + if not isinstance(value, get_args(expiry_type.value[1])): + raise ValueError( + f"The value of {expiry_type} should be of type {expiry_type.value[1]}" + ) + self.expiry_type = expiry_type + if self.expiry_type == ExpiryType.SEC: + self.cmd_arg = "EX" + if isinstance(value, timedelta): + value = int(value.total_seconds()) + elif self.expiry_type == ExpiryType.MILLSEC: + self.cmd_arg = "PX" + if isinstance(value, timedelta): + value = int(value.total_seconds() * 1000) + elif self.expiry_type == ExpiryType.UNIX_SEC: + self.cmd_arg = "EXAT" + if isinstance(value, datetime): + value = int(value.timestamp()) + elif self.expiry_type == ExpiryType.UNIX_MILLSEC: + self.cmd_arg = "PXAT" + if isinstance(value, datetime): + value = int(value.timestamp() * 1000) + elif self.expiry_type == ExpiryType.KEEP_TTL: + self.cmd_arg = "KEEPTTL" + self.value = str(value) if value else None + + def get_cmd_args(self) -> List[str]: + return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] + + +class ExpiryGetEx: + """ + GetEx option: Represents the expiry type and value to be executed with "GetEx" command. + + Attributes: + cmd_arg (str): The expiry type. + value (str): The value for the expiry type. + """ + + def __init__( + self, + expiry_type: ExpiryTypeGetEx, + value: Optional[Union[int, datetime, timedelta]], + ) -> None: + self.set_expiry_type_and_value(expiry_type, value) + + def set_expiry_type_and_value( + self, + expiry_type: ExpiryTypeGetEx, + value: Optional[Union[int, datetime, timedelta]], + ): + """ + Args: + expiry_type (ExpiryType): The expiry type. + value (Optional[Union[int, datetime, timedelta]]): The value of the expiration type. The type of expiration + determines the type of expiration value: + + - SEC: Union[int, timedelta] + - MILLSEC: Union[int, timedelta] + - UNIX_SEC: Union[int, datetime] + - UNIX_MILLSEC: Union[int, datetime] + - PERSIST: Type[None] + """ + if not isinstance(value, get_args(expiry_type.value[1])): + raise ValueError( + f"The value of {expiry_type} should be of type {expiry_type.value[1]}" + ) + self.expiry_type = expiry_type + if self.expiry_type == ExpiryTypeGetEx.SEC: + self.cmd_arg = "EX" + if isinstance(value, timedelta): + value = int(value.total_seconds()) + elif self.expiry_type == ExpiryTypeGetEx.MILLSEC: + self.cmd_arg = "PX" + if isinstance(value, timedelta): + value = int(value.total_seconds() * 1000) + elif self.expiry_type == ExpiryTypeGetEx.UNIX_SEC: + self.cmd_arg = "EXAT" + if isinstance(value, datetime): + value = int(value.timestamp()) + elif self.expiry_type == ExpiryTypeGetEx.UNIX_MILLSEC: + self.cmd_arg = "PXAT" + if isinstance(value, datetime): + value = int(value.timestamp() * 1000) + elif self.expiry_type == ExpiryTypeGetEx.PERSIST: + self.cmd_arg = "PERSIST" + self.value = str(value) if value else None + + def get_cmd_args(self) -> List[str]: + return [self.cmd_arg] if self.value is None else [self.cmd_arg, self.value] + + +class InsertPosition(Enum): + BEFORE = "BEFORE" + AFTER = "AFTER" + + +class FlushMode(Enum): + """ + Defines flushing mode for: + + `FLUSHALL` command and `FUNCTION FLUSH` command. + + See [FLUSHAL](https://valkey.io/commands/flushall/) and [FUNCTION-FLUSH](https://valkey.io/commands/function-flush/) + for details + + SYNC was introduced in version 6.2.0. + """ + + ASYNC = "ASYNC" + SYNC = "SYNC" + + +class FunctionRestorePolicy(Enum): + """ + Options for the FUNCTION RESTORE command. + """ + + APPEND = "APPEND" + """ Appends the restored libraries to the existing libraries and aborts on collision. This is the default policy. """ + + FLUSH = "FLUSH" + """ Deletes all existing libraries before restoring the payload. """ + + REPLACE = "REPLACE" + """ + Appends the restored libraries to the existing libraries, replacing any existing ones in case + of name collisions. Note that this policy doesn't prevent function name collisions, only libraries. + """ + + +def _build_sort_args( + key: TEncodable, + by_pattern: Optional[TEncodable] = None, + limit: Optional[Limit] = None, + get_patterns: Optional[List[TEncodable]] = None, + order: Optional[OrderBy] = None, + alpha: Optional[bool] = None, + store: Optional[TEncodable] = None, +) -> List[TEncodable]: + args = [key] + + if by_pattern: + args.extend(["BY", by_pattern]) + + if limit: + args.extend(["LIMIT", str(limit.offset), str(limit.count)]) + + if get_patterns: + for pattern in get_patterns: + args.extend(["GET", pattern]) + + if order: + args.append(order.value) + + if alpha: + args.append("ALPHA") + + if store: + args.extend(["STORE", store]) + + return args diff --git a/python/python/glide/async_commands/server_modules/ft.py b/python/glide/shared/commands/server_modules/ft.py similarity index 94% rename from python/python/glide/async_commands/server_modules/ft.py rename to python/glide/shared/commands/server_modules/ft.py index 0c79f3c22d..45ff0a7774 100644 --- a/python/python/glide/async_commands/server_modules/ft.py +++ b/python/glide/shared/commands/server_modules/ft.py @@ -5,24 +5,20 @@ from typing import List, Mapping, Optional, cast -from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( +from .ft_options.ft_aggregate_options import ( FtAggregateOptions, ) -from glide.async_commands.server_modules.ft_options.ft_constants import ( +from .ft_options.ft_constants import ( CommandNames, FtCreateKeywords, ) -from glide.async_commands.server_modules.ft_options.ft_create_options import ( +from .ft_options.ft_create_options import ( Field, FtCreateOptions, ) -from glide.async_commands.server_modules.ft_options.ft_profile_options import ( - FtProfileOptions, -) -from glide.async_commands.server_modules.ft_options.ft_search_options import ( - FtSearchOptions, -) -from glide.constants import ( +from .ft_options.ft_profile_options import FtProfileOptions +from .ft_options.ft_search_options import FtSearchOptions +from glide.shared.constants import ( TOK, FtAggregateResponse, FtInfoResponse, @@ -30,7 +26,7 @@ FtSearchResponse, TEncodable, ) -from glide.glide_client import TGlideClient +from glide.glide_async.python.glide.glide_client import TGlideClient # TODO: change that to work both with sync client async def create( @@ -52,7 +48,7 @@ async def create( TOK: A simple "OK" response. Examples: - >>> from glide import ft + >>> from shared import ft >>> schema: List[Field] = [TextField("title")] >>> prefixes: List[str] = ["blog:post:"] >>> await ft.create(glide_client, "my_idx1", schema, FtCreateOptions(DataType.HASH, prefixes)) @@ -81,7 +77,7 @@ async def dropindex(client: TGlideClient, index_name: TEncodable) -> TOK: Examples: For the following example to work, an index named 'idx' must be already created. If not created, you will get an error. - >>> from glide import ft + >>> from shared import ft >>> index_name = "idx" >>> await ft.dropindex(glide_client, index_name) 'OK' # Indicates successful deletion/dropping of index named 'idx' @@ -101,7 +97,7 @@ async def list(client: TGlideClient) -> List[TEncodable]: List[TEncodable]: An array of index names. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.list(glide_client) [b"index1", b"index2"] """ @@ -136,7 +132,7 @@ async def search( - An index named "idx", with fields having identifiers as "a" and "b" and prefix as "{json:}" - A key named {json:}1 with value {"a":1, "b":2} - >>> from glide import ft + >>> from shared import ft >>> await ft.search( ... glide_client, ... "idx", @@ -173,7 +169,7 @@ async def aliasadd( TOK: A simple "OK" response. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.aliasadd(glide_client, "myalias", "myindex") 'OK' # Indicates the successful addition of the alias named "myalias" for the index. """ @@ -193,7 +189,7 @@ async def aliasdel(client: TGlideClient, alias: TEncodable) -> TOK: TOK: A simple "OK" response. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.aliasdel(glide_client, "myalias") 'OK' # Indicates the successful deletion of the alias named "myalias" """ @@ -216,7 +212,7 @@ async def aliasupdate( TOK: A simple "OK" response. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.aliasupdate(glide_client, "myalias", "myindex") 'OK' # Indicates the successful update of the alias to point to the index named "myindex" """ @@ -238,7 +234,7 @@ async def info(client: TGlideClient, index_name: TEncodable) -> FtInfoResponse: Examples: An index with name 'myIndex', 1 text field and 1 vector field is already created for gettting the output of this example. - >>> from glide import ft + >>> from shared import ft >>> await ft.info(glide_client, "myIndex") [ b'index_name', @@ -311,7 +307,7 @@ async def explain( TEncodable: A string containing the parsed results representing the execution plan. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.explain(glide_client, indexName="myIndex", query="@price:[0 10]") b'Field {\n price\n 0\n 10\n}\n' # Parsed results. """ @@ -334,7 +330,7 @@ async def explaincli( List[TEncodable]: An array containing the execution plan. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.explaincli(glide_client, indexName="myIndex", query="@price:[0 10]") [b'Field {', b' price', b' 0', b' 10', b'}', b''] # Parsed results. """ @@ -363,7 +359,7 @@ async def aggregate( of the command. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft.aggregate( ... glide_client, ... "myIndex", @@ -456,7 +452,7 @@ async def aliaslist(client: TGlideClient) -> Mapping[TEncodable, TEncodable]: Returns: Mapping[TEncodable, TEncodable]: A map of index aliases for indices being aliased. Examples: - >>> from glide import ft + >>> from shared import ft >>> await ft._aliaslist(glide_client) {b'alias': b'index1', b'alias-bytes': b'index2'} """ diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_aggregate_options.py b/python/glide/shared/commands/server_modules/ft_options/ft_aggregate_options.py similarity index 98% rename from python/python/glide/async_commands/server_modules/ft_options/ft_aggregate_options.py rename to python/glide/shared/commands/server_modules/ft_options/ft_aggregate_options.py index d306652c52..97f83a5464 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_aggregate_options.py +++ b/python/glide/shared/commands/server_modules/ft_options/ft_aggregate_options.py @@ -2,11 +2,9 @@ from abc import ABC, abstractmethod from typing import List, Mapping, Optional -from glide.async_commands.command_args import OrderBy -from glide.async_commands.server_modules.ft_options.ft_constants import ( - FtAggregateKeywords, -) -from glide.constants import TEncodable +from ...command_args import OrderBy +from .ft_constants import FtAggregateKeywords +from glide.shared.constants import TEncodable class FtAggregateClause(ABC): diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_constants.py b/python/glide/shared/commands/server_modules/ft_options/ft_constants.py similarity index 100% rename from python/python/glide/async_commands/server_modules/ft_options/ft_constants.py rename to python/glide/shared/commands/server_modules/ft_options/ft_constants.py diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_create_options.py b/python/glide/shared/commands/server_modules/ft_options/ft_create_options.py similarity index 99% rename from python/python/glide/async_commands/server_modules/ft_options/ft_create_options.py rename to python/glide/shared/commands/server_modules/ft_options/ft_create_options.py index fc7d5bb769..15e64b10b3 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_create_options.py +++ b/python/glide/shared/commands/server_modules/ft_options/ft_create_options.py @@ -3,8 +3,8 @@ from enum import Enum from typing import List, Optional -from glide.async_commands.server_modules.ft_options.ft_constants import FtCreateKeywords -from glide.constants import TEncodable +from .ft_constants import FtCreateKeywords +from glide.shared.constants import TEncodable class FieldType(Enum): diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_profile_options.py b/python/glide/shared/commands/server_modules/ft_options/ft_profile_options.py similarity index 92% rename from python/python/glide/async_commands/server_modules/ft_options/ft_profile_options.py rename to python/glide/shared/commands/server_modules/ft_options/ft_profile_options.py index 114d72132f..a5b738dcf8 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_profile_options.py +++ b/python/glide/shared/commands/server_modules/ft_options/ft_profile_options.py @@ -2,16 +2,12 @@ from enum import Enum from typing import List, Optional, Union, cast -from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( +from .ft_aggregate_options import ( FtAggregateOptions, ) -from glide.async_commands.server_modules.ft_options.ft_constants import ( - FtProfileKeywords, -) -from glide.async_commands.server_modules.ft_options.ft_search_options import ( - FtSearchOptions, -) -from glide.constants import TEncodable +from .ft_constants import FtProfileKeywords +from .ft_search_options import FtSearchOptions +from glide.shared.constants import TEncodable class QueryType(Enum): diff --git a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py b/python/glide/shared/commands/server_modules/ft_options/ft_search_options.py similarity index 97% rename from python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py rename to python/glide/shared/commands/server_modules/ft_options/ft_search_options.py index 8fc4cfc6ac..3d1ca1d2a9 100644 --- a/python/python/glide/async_commands/server_modules/ft_options/ft_search_options.py +++ b/python/glide/shared/commands/server_modules/ft_options/ft_search_options.py @@ -2,8 +2,8 @@ from typing import List, Mapping, Optional -from glide.async_commands.server_modules.ft_options.ft_constants import FtSearchKeywords -from glide.constants import TEncodable +from .ft_constants import FtSearchKeywords +from glide.shared.constants import TEncodable class FtSearchLimit: diff --git a/python/python/glide/async_commands/server_modules/glide_json.py b/python/glide/shared/commands/server_modules/glide_json.py similarity index 97% rename from python/python/glide/async_commands/server_modules/glide_json.py rename to python/glide/shared/commands/server_modules/glide_json.py index 927e806006..fc1cbe84c2 100644 --- a/python/python/glide/async_commands/server_modules/glide_json.py +++ b/python/glide/shared/commands/server_modules/glide_json.py @@ -3,7 +3,7 @@ Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> value = {'a': 1.0, 'b': 2} >>> json_str = json.dumps(value) # Convert Python dictionary to JSON string using json.dumps() @@ -19,9 +19,9 @@ """ from typing import List, Optional, Union, cast -from glide.async_commands.core import ConditionalChange -from glide.constants import TOK, TEncodable, TJsonResponse, TJsonUniversalResponse -from glide.glide_client import TGlideClient +from glide.shared.commands.core_options import ConditionalChange +from glide.shared.constants import TOK, TEncodable, TJsonResponse, TJsonUniversalResponse +from glide.glide_async.python.glide.glide_client import TGlideClient # TODO: change that to support both sync client class JsonGetOptions: @@ -137,7 +137,7 @@ async def set( If `value` isn't set because of `set_condition`, returns None. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> value = {'a': 1.0, 'b': 2} >>> json_str = json.dumps(value) @@ -187,7 +187,7 @@ async def get( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json, JsonGetOptions + >>> from shared import glide_json, JsonGetOptions >>> import json >>> json_str = await glide_json.get(client, "doc", "$") >>> json.loads(str(json_str)) # Parse JSON string to Python data @@ -244,7 +244,7 @@ async def arrappend( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> await glide_json.set(client, "doc", "$", '{"a": 1, "b": ["one", "two"]}') 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. @@ -307,7 +307,7 @@ async def arrindex( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '[[], ["a"], ["a", "b"], ["a", "b", "c"]]') 'OK' >>> await glide_json.arrindex(client, "doc", "$[*]", '"b"') @@ -371,7 +371,7 @@ async def arrinsert( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '[[], ["a"], ["a", "b"]]') 'OK' >>> await glide_json.arrinsert(client, "doc", "$[*]", 0, ['"c"', '{"key": "value"}', "true", "null", '["bar"]']) @@ -417,7 +417,7 @@ async def arrlen( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": [1, 2, 3], "b": {"a": [1, 2], "c": {"a": 42}}}') 'OK' # JSON is successfully set for doc >>> await glide_json.arrlen(client, "doc", "$") @@ -476,7 +476,7 @@ async def arrpop( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set( ... client, ... "doc", @@ -551,7 +551,7 @@ async def arrtrim( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '[[], ["a"], ["a", "b"], ["a", "b", "c"]]') 'OK' >>> await glide_json.arrtrim(client, "doc", "$[*]", 0, 1) @@ -594,7 +594,7 @@ async def clear( If `key doesn't exist, an error is raised. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set( ... client, ... "doc", @@ -676,7 +676,7 @@ async def debug_fields( For more information about the returned type, see `TJsonUniversalResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "k1", "$", '[1, 2.3, "foo", true, null, {}, [], {"a":1, "b":2}, [1,2,3]]') 'OK' >>> await glide_json.debug_fields(client, "k1", "$[*]") @@ -742,7 +742,7 @@ async def debug_memory( For more information about the returned type, see `TJsonUniversalResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "k1", "$", '[1, 2.3, "foo", true, null, {}, [], {"a":1, "b":2}, [1,2,3]]') 'OK' >>> await glide_json.debug_memory(client, "k1", "$[*]") @@ -798,7 +798,7 @@ async def delete( If `key` or `path` doesn't exist, returns 0. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. >>> await glide_json.delete(client, "doc", "$..a") @@ -833,7 +833,7 @@ async def forget( If `key` or `path` doesn't exist, returns 0. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. >>> await glide_json.forget(client, "doc", "$..a") @@ -883,7 +883,7 @@ async def mget( Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> json_strs = await glide_json.mget(client, ["doc1", "doc2"], "$") >>> [json.loads(js) for js in json_strs] # Parse JSON strings to Python data @@ -929,7 +929,7 @@ async def numincrby( If the result is out of the range of 64-bit IEEE double, an error is raised. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": [], "b": [1], "c": [1, 2], "d": [1, 2, 3]}') 'OK' >>> await glide_json.numincrby(client, "doc", "$.d[*]", 10) @@ -972,7 +972,7 @@ async def nummultby( If the result is out of the range of 64-bit IEEE double, an error is raised. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": [], "b": [1], "c": [1, 2], "d": [1, 2, 3]}') 'OK' >>> await glide_json.nummultby(client, "doc", "$.d[*]", 2) @@ -1014,7 +1014,7 @@ async def objlen( Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": 1.0, "b": {"a": {"x": 1, "y": 2}, "b": 2.5, "c": true}}') b'OK' # Indicates successful setting of the value at the root path '$' in the key `doc`. >>> await glide_json.objlen(client, "doc", "$") @@ -1068,7 +1068,7 @@ async def objkeys( For more information about the returned type, see `TJsonUniversalResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": 1.0, "b": {"a": {"x": 1, "y": 2}, "b": 2.5, "c": true}}') b'OK' # Indicates successful setting of the value at the root path '$' in the key `doc`. >>> await glide_json.objkeys(client, "doc", "$") @@ -1127,7 +1127,7 @@ async def resp( For more information about the returned type, see `TJsonUniversalResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": [1, 2, 3], "b": {"a": [1, 2], "c": {"a": 42}}}') 'OK' >>> await glide_json.resp(client, "doc", "$..a") @@ -1178,7 +1178,7 @@ async def strappend( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> await glide_json.set(client, "doc", "$", json.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})) 'OK' @@ -1227,7 +1227,7 @@ async def strlen( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> await glide_json.set(client, "doc", "$", json.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})) 'OK' @@ -1276,7 +1276,7 @@ async def toggle( For more information about the returned type, see `TJsonResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> import json >>> await glide_json.set( ... client, @@ -1327,7 +1327,7 @@ async def type( For more information about the returned type, see `TJsonUniversalResponse`. Examples: - >>> from glide import glide_json + >>> from shared import glide_json >>> await glide_json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') 'OK' >>> await glide_json.type(client, "doc", "$.nested") diff --git a/python/python/glide/async_commands/server_modules/json_batch.py b/python/glide/shared/commands/server_modules/json_batch.py similarity index 99% rename from python/python/glide/async_commands/server_modules/json_batch.py rename to python/glide/shared/commands/server_modules/json_batch.py index 5dac408e3b..9b2b3dc332 100644 --- a/python/python/glide/async_commands/server_modules/json_batch.py +++ b/python/glide/shared/commands/server_modules/json_batch.py @@ -3,7 +3,7 @@ Examples: >>> import json - >>> from glide import json_batch + >>> from shared import json_batch >>> batch = ClusterBatch(is_atomic=True) >>> value = {'a': 1.0, 'b': 2} >>> json_str = json.dumps(value) # Convert Python dictionary to JSON string using json.dumps() @@ -22,14 +22,14 @@ from typing import List, Optional, Union -from glide.async_commands.batch import TBatch -from glide.async_commands.core import ConditionalChange -from glide.async_commands.server_modules.glide_json import ( +from glide.shared.commands.batch import TBatch +from glide.shared.commands.core_options import ConditionalChange +from glide.shared.commands.server_modules.glide_json import ( JsonArrIndexOptions, JsonArrPopOptions, JsonGetOptions, ) -from glide.constants import TEncodable +from glide.shared.constants import TEncodable def set( diff --git a/python/python/glide/async_commands/sorted_set.py b/python/glide/shared/commands/sorted_set.py similarity index 99% rename from python/python/glide/async_commands/sorted_set.py rename to python/glide/shared/commands/sorted_set.py index 9ca9456453..ebd701d153 100644 --- a/python/python/glide/async_commands/sorted_set.py +++ b/python/glide/shared/commands/sorted_set.py @@ -3,8 +3,8 @@ from enum import Enum from typing import List, Optional, Tuple, Union, cast -from glide.async_commands.command_args import Limit, OrderBy -from glide.constants import TEncodable +from ..commands.command_args import Limit, OrderBy +from glide.shared.constants import TEncodable class InfBound(Enum): diff --git a/python/python/glide/async_commands/stream.py b/python/glide/shared/commands/stream.py similarity index 99% rename from python/python/glide/async_commands/stream.py rename to python/glide/shared/commands/stream.py index 430ad503bd..9446568d57 100644 --- a/python/python/glide/async_commands/stream.py +++ b/python/glide/shared/commands/stream.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from typing import List, Optional, Union -from glide.constants import TEncodable +from glide.shared.constants import TEncodable class StreamTrimOptions(ABC): diff --git a/python/python/glide/config.py b/python/glide/shared/config.py similarity index 96% rename from python/python/glide/config.py rename to python/glide/shared/config.py index 529292c136..57819f9d87 100644 --- a/python/python/glide/config.py +++ b/python/glide/shared/config.py @@ -6,12 +6,12 @@ from enum import Enum, IntEnum from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union -from glide.async_commands.core import CoreCommands -from glide.exceptions import ConfigurationError -from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.protobuf.connection_request_pb2 import ProtocolVersion as SentProtocolVersion -from glide.protobuf.connection_request_pb2 import ReadFrom as ProtobufReadFrom -from glide.protobuf.connection_request_pb2 import TlsMode +from .commands.core_options import PubSubMsg +from glide.shared.exceptions import ConfigurationError +from .protobuf.connection_request_pb2 import ConnectionRequest +from .protobuf.connection_request_pb2 import ProtocolVersion as SentProtocolVersion +from .protobuf.connection_request_pb2 import ReadFrom as ProtobufReadFrom +from .protobuf.connection_request_pb2 import TlsMode class NodeAddress: @@ -308,7 +308,7 @@ def _is_pubsub_configured(self) -> bool: def _get_pubsub_callback_and_context( self, - ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + ) -> Tuple[Optional[Callable[[PubSubMsg, Any], None]], Any]: return None, None @@ -387,7 +387,7 @@ class PubSubSubscriptions: Attributes: channels_and_patterns (Dict[GlideClientConfiguration.PubSubChannelModes, Set[str]]): Channels and patterns by modes. - callback (Optional[Callable[[CoreCommands.PubSubMsg, Any], None]]): + callback (Optional[Callable[[PubSubMsg, Any], None]]): Optional callback to accept the incomming messages. context (Any): Arbitrary context to pass to the callback. @@ -396,7 +396,7 @@ class PubSubSubscriptions: channels_and_patterns: Dict[ GlideClientConfiguration.PubSubChannelModes, Set[str] ] - callback: Optional[Callable[[CoreCommands.PubSubMsg, Any], None]] + callback: Optional[Callable[[PubSubMsg, Any], None]] context: Any def __init__( @@ -468,7 +468,7 @@ def _is_pubsub_configured(self) -> bool: def _get_pubsub_callback_and_context( self, - ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + ) -> Tuple[Optional[Callable[[PubSubMsg, Any], None]], Any]: if self.pubsub_subscriptions: return self.pubsub_subscriptions.callback, self.pubsub_subscriptions.context return None, None @@ -557,7 +557,7 @@ class PubSubSubscriptions: Attributes: channels_and_patterns (Dict[GlideClusterClientConfiguration.PubSubChannelModes, Set[str]]): Channels and patterns by modes. - callback (Optional[Callable[[CoreCommands.PubSubMsg, Any], None]]): + callback (Optional[Callable[[PubSubMsg, Any], None]]): Optional callback to accept the incoming messages. context (Any): Arbitrary context to pass to the callback. @@ -566,7 +566,7 @@ class PubSubSubscriptions: channels_and_patterns: Dict[ GlideClusterClientConfiguration.PubSubChannelModes, Set[str] ] - callback: Optional[Callable[[CoreCommands.PubSubMsg, Any], None]] + callback: Optional[Callable[[PubSubMsg, Any], None]] context: Any def __init__( @@ -644,7 +644,7 @@ def _is_pubsub_configured(self) -> bool: def _get_pubsub_callback_and_context( self, - ) -> Tuple[Optional[Callable[[CoreCommands.PubSubMsg, Any], None]], Any]: + ) -> Tuple[Optional[Callable[[PubSubMsg, Any], None]], Any]: if self.pubsub_subscriptions: return self.pubsub_subscriptions.callback, self.pubsub_subscriptions.context return None, None diff --git a/python/python/glide/constants.py b/python/glide/shared/constants.py similarity index 95% rename from python/python/glide/constants.py rename to python/glide/shared/constants.py index 28d7db390d..f3c1337bdc 100644 --- a/python/python/glide/constants.py +++ b/python/glide/shared/constants.py @@ -2,9 +2,9 @@ from typing import Any, Dict, List, Literal, Mapping, Optional, Set, TypeVar, Union -from glide.protobuf.command_request_pb2 import CommandRequest -from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.routes import ByAddressRoute, RandomNode, SlotIdRoute, SlotKeyRoute +from .protobuf.command_request_pb2 import CommandRequest +from .protobuf.connection_request_pb2 import ConnectionRequest +from glide.shared.routes import ByAddressRoute, RandomNode, SlotIdRoute, SlotKeyRoute OK: str = "OK" DEFAULT_READ_BYTES_SIZE: int = pow(2, 16) diff --git a/python/python/glide/exceptions.py b/python/glide/shared/exceptions.py similarity index 68% rename from python/python/glide/exceptions.py rename to python/glide/shared/exceptions.py index 5c5659974c..a79d14a340 100644 --- a/python/python/glide/exceptions.py +++ b/python/glide/shared/exceptions.py @@ -1,6 +1,8 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from typing import Optional +from typing import Optional, Type + +from .protobuf.response_pb2 import RequestErrorType class GlideError(Exception): @@ -60,3 +62,16 @@ class ConfigurationError(RequestError): """ Errors that are thrown when a request cannot be completed in current configuration settings. """ + +def get_request_error_class( + error_type: Optional[RequestErrorType.ValueType], +) -> Type[RequestError]: + if error_type == RequestErrorType.Disconnect: + return ConnectionError + if error_type == RequestErrorType.ExecAbort: + return ExecAbortError + if error_type == RequestErrorType.Timeout: + return TimeoutError + if error_type == RequestErrorType.Unspecified: + return RequestError + return RequestError diff --git a/python/python/glide/protobuf_codec.py b/python/glide/shared/protobuf_codec.py similarity index 100% rename from python/python/glide/protobuf_codec.py rename to python/glide/shared/protobuf_codec.py diff --git a/python/python/glide/routes.py b/python/glide/shared/routes.py similarity index 94% rename from python/python/glide/routes.py rename to python/glide/shared/routes.py index a15f62793d..843b45ca3c 100644 --- a/python/python/glide/routes.py +++ b/python/glide/shared/routes.py @@ -3,9 +3,9 @@ from enum import Enum from typing import Optional -from glide.exceptions import RequestError -from glide.protobuf.command_request_pb2 import CommandRequest, SimpleRoutes -from glide.protobuf.command_request_pb2 import SlotTypes as ProtoSlotTypes +from glide.shared.exceptions import RequestError +from .protobuf.command_request_pb2 import CommandRequest, SimpleRoutes +from .protobuf.command_request_pb2 import SlotTypes as ProtoSlotTypes class SlotType(Enum): diff --git a/python/pyproject.toml b/python/pyproject.toml index cdc14d2ce6..ad8246b13e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,39 +1,18 @@ [build-system] -requires = ["maturin==0.14.17"] -build-backend = "maturin" +requires = ["setuptools>=61.0.0"] +build-backend = "setuptools.build_meta" [project] -name = "valkey-glide" -description = "An open source Valkey client library that supports Valkey and Redis open source 6.2, 7.0, 7.2 and 8.0." -requires-python = ">=3.9" -dependencies = [ - # Note: If you add a dependency here, make sure to also add it to requirements.txt - # Once issue https://github.com/aboutcode-org/python-inspector/issues/197 is resolved, the requirements.txt file can be removed. - "async-timeout>=4.0.2; python_version < '3.11'", - "typing-extensions>=4.8.0; python_version < '3.11'", - "protobuf>=3.20", -] +name = "test-glide-meta" +version = "0.1.0" +description = "Meta client for test-glide-meta, combining sync and async clients" +license = {text = "MIT"} -classifiers = [ - "Topic :: Database", - "Topic :: Utilities", - "License :: OSI Approved :: Apache Software License", - "Intended Audience :: Developers", - "Topic :: Software Development", - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] +[project.optional-dependencies] +sync = ["test-glide-async"] +async = ["test-glide-sync"] +full = ["test-glide-async", "test-glide-sync"] -[tool.isort] -profile = "black" -skip = [ - ".env", - "python/glide/protobuf" -] - -[tool.black] -target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] - -[tool.mypy] -exclude = "^(.*\\/)?(\\.env|python/python/glide/protobuf|utils/release-candidate-testing|target|ort)(\\/|$)" +[tool.setuptools] +packages = ["test-glide-meta"] +package-dir = {"" = ".."} diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 5d0f0609bf..9f6db4ae27 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,10 +1,10 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import random -from typing import AsyncGenerator, List, Optional, Union +from typing import AsyncGenerator, Generator, List, Optional, Union import pytest -from glide.config import ( +from glide.shared.config import ( AdvancedGlideClientConfiguration, AdvancedGlideClusterClientConfiguration, BackoffStrategy, @@ -15,16 +15,19 @@ ReadFrom, ServerCredentials, ) -from glide.exceptions import ClosingError -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient -from glide.logger import Level as logLevel -from glide.logger import Logger -from glide.routes import AllNodes +from glide.shared.exceptions import ClosingError +from glide.glide_async.python.glide import GlideClient, GlideClusterClient, TGlideClient # TODO: change that to support both sync client +from glide.glide_async.python.glide.logger import Level as logLevel +from glide.glide_async.python.glide.logger import Logger +from glide.shared.routes import AllNodes +from glide.glide_sync.glide_sync.glide_client import GlideClient as SyncGlideClient +from glide.glide_sync.glide_sync import GlideClusterClient as SyncGlideClusterClient +from glide.glide_sync.glide_sync import TGlideClient as TSyncGlideClient from tests.utils.cluster import ValkeyCluster from tests.utils.utils import ( - check_if_server_version_lt, set_new_acl_username_with_password, + sync_check_if_server_version_lt, ) DEFAULT_HOST = "localhost" @@ -230,6 +233,19 @@ async def glide_client( await client.close() +@pytest.fixture(scope="function") +def glide_sync_client( + request, + cluster_mode: bool, + protocol: ProtocolVersion, +) -> Generator[TSyncGlideClient, None, None]: + "Get async socket client for tests" + client = create_sync_client(request, cluster_mode, protocol=protocol) + yield client + sync_test_teardown(request, cluster_mode, protocol) + client.close() + + @pytest.fixture(scope="function") async def management_client( request, @@ -272,7 +288,7 @@ async def acl_glide_client( await client.close() -async def create_client( +def create_client_config( request, cluster_mode: bool, credentials: Optional[ServerCredentials] = None, @@ -280,7 +296,7 @@ async def create_client( addresses: Optional[List[NodeAddress]] = None, client_name: Optional[str] = None, protocol: ProtocolVersion = ProtocolVersion.RESP3, - request_timeout: Optional[int] = 1000, + timeout: Optional[int] = 1000, connection_timeout: Optional[int] = 1000, cluster_mode_pubsub: Optional[ GlideClusterClientConfiguration.PubSubSubscriptions @@ -293,8 +309,7 @@ async def create_client( client_az: Optional[str] = None, reconnect_strategy: Optional[BackoffStrategy] = None, valkey_cluster: Optional[ValkeyCluster] = None, -) -> Union[GlideClient, GlideClusterClient]: - # Create async socket client +) -> Union[GlideClusterClientConfiguration, GlideClientConfiguration]: use_tls = request.config.getoption("--tls") if cluster_mode: valkey_cluster = valkey_cluster or pytest.valkey_cluster # type: ignore @@ -302,20 +317,19 @@ async def create_client( assert database_id == 0 k = min(3, len(valkey_cluster.nodes_addr)) seed_nodes = random.sample(valkey_cluster.nodes_addr, k=k) - cluster_config = GlideClusterClientConfiguration( + config = GlideClusterClientConfiguration( addresses=seed_nodes if addresses is None else addresses, use_tls=use_tls, credentials=credentials, client_name=client_name, protocol=protocol, - request_timeout=request_timeout, + request_timeout=timeout, pubsub_subscriptions=cluster_mode_pubsub, inflight_requests_limit=inflight_requests_limit, read_from=read_from, client_az=client_az, advanced_config=AdvancedGlideClusterClientConfiguration(connection_timeout), ) - return await GlideClusterClient.create(cluster_config) else: assert type(pytest.standalone_cluster) is ValkeyCluster # type: ignore config = GlideClientConfiguration( @@ -327,17 +341,110 @@ async def create_client( database_id=database_id, client_name=client_name, protocol=protocol, - request_timeout=request_timeout, + request_timeout=timeout, pubsub_subscriptions=standalone_mode_pubsub, inflight_requests_limit=inflight_requests_limit, read_from=read_from, client_az=client_az, - advanced_config=AdvancedGlideClientConfiguration(connection_timeout), reconnect_strategy=reconnect_strategy, + advanced_config=AdvancedGlideClientConfiguration(connection_timeout), ) + return config + + +async def create_client( + request, + cluster_mode: bool, + credentials: Optional[ServerCredentials] = None, + database_id: int = 0, + addresses: Optional[List[NodeAddress]] = None, + client_name: Optional[str] = None, + protocol: ProtocolVersion = ProtocolVersion.RESP3, + request_timeout: Optional[int] = 1000, + connection_timeout: Optional[int] = 1000, + cluster_mode_pubsub: Optional[ + GlideClusterClientConfiguration.PubSubSubscriptions + ] = None, + standalone_mode_pubsub: Optional[ + GlideClientConfiguration.PubSubSubscriptions + ] = None, + inflight_requests_limit: Optional[int] = None, + read_from: ReadFrom = ReadFrom.PRIMARY, + client_az: Optional[str] = None, + reconnect_strategy: Optional[BackoffStrategy] = None, + valkey_cluster: Optional[ValkeyCluster] = None, +) -> Union[GlideClient, GlideClusterClient]: + config = create_client_config( + request, + cluster_mode, + credentials, + database_id, + addresses, + client_name, + protocol, + request_timeout, + connection_timeout, + cluster_mode_pubsub, + standalone_mode_pubsub, + inflight_requests_limit, + read_from, + client_az, + reconnect_strategy, + valkey_cluster, + ) + if cluster_mode: + return await GlideClusterClient.create(config) + else: return await GlideClient.create(config) +def create_sync_client( + request, + cluster_mode: bool, + credentials: Optional[ServerCredentials] = None, + database_id: int = 0, + addresses: Optional[List[NodeAddress]] = None, + client_name: Optional[str] = None, + protocol: ProtocolVersion = ProtocolVersion.RESP3, + timeout: Optional[int] = 1000, + connection_timeout: Optional[int] = 1000, + cluster_mode_pubsub: Optional[ + GlideClusterClientConfiguration.PubSubSubscriptions + ] = None, + standalone_mode_pubsub: Optional[ + GlideClientConfiguration.PubSubSubscriptions + ] = None, + inflight_requests_limit: Optional[int] = None, + read_from: ReadFrom = ReadFrom.PRIMARY, + client_az: Optional[str] = None, + reconnect_strategy: Optional[BackoffStrategy] = None, + valkey_cluster: Optional[ValkeyCluster] = None, +) -> TSyncGlideClient: + # Create sync client + config = create_client_config( + request, + cluster_mode, + credentials, + database_id, + addresses, + client_name, + protocol, + timeout, + connection_timeout, + cluster_mode_pubsub, + standalone_mode_pubsub, + inflight_requests_limit, + read_from, + client_az, + reconnect_strategy, + valkey_cluster, + ) + if cluster_mode: + return SyncGlideClusterClient.create(config) + else: + return SyncGlideClient.create(config) + + USERNAME = "username" INITIAL_PASSWORD = "initial_password" NEW_PASSWORD = "new_secure_password" @@ -356,7 +463,28 @@ async def auth_client(client: TGlideClient, password: str, username: str = "defa ) -async def config_set_new_password(client: TGlideClient, password: str): +def sync_auth_client(client: TSyncGlideClient, password): + """ + Authenticates the given TGlideClient server connected. + """ + if isinstance(client, GlideClient): + client.custom_command(["AUTH", password]) + elif isinstance(client, GlideClusterClient): + client.custom_command(["AUTH", password], route=AllNodes()) + + +def sync_config_set_new_password(client: TSyncGlideClient, password): + """ + Sets a new password for the given TGlideClient server connected. + This function updates the server to require a new password. + """ + if isinstance(client, GlideClient): + client.config_set({"requirepass": password}) + elif isinstance(client, GlideClusterClient): + client.config_set({"requirepass": password}, route=AllNodes()) + + +async def config_set_new_password(client: TGlideClient, password): """ Sets a new password for the given TGlideClient server connected. This function updates the server to require a new password. @@ -367,6 +495,16 @@ async def config_set_new_password(client: TGlideClient, password: str): await client.config_set({"requirepass": password}, route=AllNodes()) +def sync_kill_connections(client: TSyncGlideClient): + """ + Kills all connections to the given TGlideClient server connected. + """ + if isinstance(client, GlideClient): + client.custom_command(["CLIENT", "KILL", "TYPE", "normal"]) + elif isinstance(client, GlideClusterClient): + client.custom_command(["CLIENT", "KILL", "TYPE", "normal"], route=AllNodes()) + + async def kill_connections(client: TGlideClient): """ Kills all connections to the given TGlideClient server connected. @@ -419,8 +557,48 @@ async def test_teardown(request, cluster_mode: bool, protocol: ProtocolVersion): raise e +def sync_test_teardown(request, cluster_mode: bool, protocol: ProtocolVersion): + """ + Perform teardown tasks such as flushing all data from the cluster. + + If authentication is required, attempt to connect with the known password, + reset it back to empty, and proceed with teardown. + """ + credentials = None + try: + # Try connecting without credentials + client = create_sync_client( + request, cluster_mode, protocol=protocol, timeout=2000 + ) + client.custom_command(["FLUSHALL"]) + client.close() + except ClosingError as e: + # Check if the error is due to authentication + if "NOAUTH" in str(e): + # Use the known password to authenticate + credentials = ServerCredentials(password=NEW_PASSWORD) + client = create_sync_client( + request, + cluster_mode, + protocol=protocol, + timeout=2000, + credentials=credentials, + ) + try: + sync_auth_client(client, NEW_PASSWORD) + # Reset the server password back to empty + sync_config_set_new_password(client, "") + client.update_connection_password(None) + # Perform the teardown + client.custom_command(["FLUSHALL"]) + finally: + client.close() + else: + raise e + + @pytest.fixture(autouse=True) -async def skip_if_version_below(request): +def skip_if_version_below(request): """ Skip test(s) if server version is below than given parameter. Can skip a complete test suite. @@ -431,8 +609,8 @@ async def test_meow_meow(...): """ if request.node.get_closest_marker("skip_if_version_below"): min_version = request.node.get_closest_marker("skip_if_version_below").args[0] - client = await create_client(request, False) - if await check_if_server_version_lt(client, min_version): + client = create_sync_client(request, False) + if sync_check_if_server_version_lt(client, min_version): pytest.skip( reason=f"This feature added in version {min_version}", allow_module_level=True, diff --git a/python/tests/test_api_export.py b/python/tests/test_api_export.py index 18cab3ac39..d332515d1b 100644 --- a/python/tests/test_api_export.py +++ b/python/tests/test_api_export.py @@ -2,14 +2,14 @@ import ast from pathlib import Path -import glide +import glide.shared as glide exported_symbol_list = glide.__all__ def _get_export_rename_map(): glide.__all__ - root_init_file = Path(__file__).parent.parent / "python" / "glide" / "__init__.py" + root_init_file = Path(__file__).parent.parent / "glide" / "shared" / "__init__.py" source_code = root_init_file.read_text() tree = ast.parse(source_code) rename_map = {} @@ -31,6 +31,8 @@ def _get_export_rename_map(): # python/python/glide/glide_client.py "get_request_error_class", # FunctionDef "BaseClient", # ClassDef + # python/python/glide/sync/glide_client.py + "FFIClientTypeEnum", # ClassDef # python/python/glide/routes.py "to_protobuf_slot_type", # FunctionDef "set_protobuf_route", # FunctionDef @@ -43,13 +45,13 @@ def _get_export_rename_map(): "BaseBatch", # ClassDef # python/python/glide/async_commands/standalone_commands.py "StandaloneCommands", # ClassDef - # python/python/glide/async_commands/cluster_commands.py + # python/python/glide/commands/async_commands/cluster_commands.py "ClusterCommands", # ClassDef - # python/python/glide/async_commands/core.py + # python/python/glide/commands/async_commands/core.py "CoreCommands", # ClassDef - # python/python/glide/async_commands/sorted_set.py + # python/python/glide/commands/async_commands/sorted_set.py "separate_keys", # FunctionDef - # python/python/glide/async_commands/server_modules/ft_options/ft_constants.py + # python/python/glide/commands/async_commands/server_modules/ft_options/ft_constants.py "CommandNames", # ClassDef "FtCreateKeywords", # ClassDef "FtSearchKeywords", # ClassDef diff --git a/python/tests/test_async_client.py b/python/tests/test_async_client.py index d015eef566..8917bebc4a 100644 --- a/python/tests/test_async_client.py +++ b/python/tests/test_async_client.py @@ -10,9 +10,9 @@ from typing import Any, Dict, List, Mapping, Optional, Union, cast import pytest -from glide import ClosingError, RequestError, Script -from glide.async_commands.batch import Batch, ClusterBatch -from glide.async_commands.bitmap import ( +from shared import ClosingError, RequestError, Script +from glide.shared.commands.batch import Batch, ClusterBatch +from glide.shared.commands.bitmap import ( BitFieldGet, BitFieldIncrBy, BitFieldOverflow, @@ -26,8 +26,8 @@ SignedEncoding, UnsignedEncoding, ) -from glide.async_commands.command_args import Limit, ListDirection, OrderBy -from glide.async_commands.core import ( +from glide.shared.commands.command_args import Limit, ListDirection, OrderBy +from glide.shared.commands.core_options import ( ConditionalChange, ExpireOptions, ExpiryGetEx, @@ -36,19 +36,19 @@ ExpiryTypeGetEx, FlushMode, FunctionRestorePolicy, - InfBound, InfoSection, InsertPosition, OnlyIfEqual, UpdateOptions, ) -from glide.async_commands.sorted_set import ( +from glide.shared.commands.sorted_set import ( AggregationType, GeoSearchByBox, GeoSearchByRadius, GeoSearchCount, GeospatialData, GeoUnit, + InfBound, LexBoundary, RangeByIndex, RangeByLex, @@ -56,7 +56,7 @@ ScoreBoundary, ScoreFilter, ) -from glide.async_commands.stream import ( +from glide.shared.commands.stream import ( ExclusiveIdBound, IdBound, MaxId, @@ -70,10 +70,10 @@ TrimByMaxLen, TrimByMinId, ) -from glide.config import BackoffStrategy, ProtocolVersion, ServerCredentials -from glide.constants import OK, TEncodable, TFunctionStatsSingleNodeResponse, TResult -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient -from glide.routes import ( +from glide.shared.config import BackoffStrategy, ProtocolVersion, ServerCredentials +from glide.shared.constants import OK, TEncodable, TFunctionStatsSingleNodeResponse, TResult +from glide.glide_async.python.glide import GlideClient, GlideClusterClient, TGlideClient +from glide.shared.routes import ( AllNodes, AllPrimaries, ByAddressRoute, diff --git a/python/tests/test_auth.py b/python/tests/test_auth.py index 6666041796..1f9d80e7a9 100644 --- a/python/tests/test_auth.py +++ b/python/tests/test_auth.py @@ -3,10 +3,10 @@ import asyncio import pytest -from glide.config import ProtocolVersion -from glide.constants import OK -from glide.exceptions import RequestError -from glide.glide_client import TGlideClient +from glide.shared.config import ProtocolVersion +from glide.shared.constants import OK +from glide.shared.exceptions import RequestError +from glide.glide_async.python.glide import TGlideClient from tests.conftest import ( NEW_PASSWORD, diff --git a/python/tests/test_batch.py b/python/tests/test_batch.py index c21e02909e..54c2d7bee9 100644 --- a/python/tests/test_batch.py +++ b/python/tests/test_batch.py @@ -6,15 +6,15 @@ from typing import List, Optional, Union, cast import pytest -from glide import RequestError, TimeoutError -from glide.async_commands.batch import ( +from shared import RequestError, TimeoutError +from glide.shared.commands.batch import ( BaseBatch, Batch, ClusterBatch, ClusterTransaction, Transaction, ) -from glide.async_commands.bitmap import ( +from glide.shared.commands.bitmap import ( BitFieldGet, BitFieldSet, BitmapIndexType, @@ -25,8 +25,8 @@ SignedEncoding, UnsignedEncoding, ) -from glide.async_commands.command_args import Limit, ListDirection, OrderBy -from glide.async_commands.core import ( +from glide.shared.commands.command_args import Limit, ListDirection, OrderBy +from glide.shared.commands.core_options import ( ExpiryGetEx, ExpiryTypeGetEx, FlushMode, @@ -34,7 +34,7 @@ InfoSection, InsertPosition, ) -from glide.async_commands.sorted_set import ( +from glide.shared.commands.sorted_set import ( AggregationType, GeoSearchByBox, GeoSearchByRadius, @@ -46,7 +46,7 @@ ScoreBoundary, ScoreFilter, ) -from glide.async_commands.stream import ( +from glide.shared.commands.stream import ( IdBound, MaxId, MinId, @@ -56,10 +56,10 @@ StreamReadGroupOptions, TrimByMinId, ) -from glide.config import ProtocolVersion -from glide.constants import OK, TResult, TSingleNodeRoute -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient -from glide.routes import AllNodes, SlotIdRoute, SlotKeyRoute, SlotType +from glide.shared.config import ProtocolVersion +from glide.shared.constants import OK, TResult, TSingleNodeRoute +from glide.glide_async.python.glide import GlideClient, GlideClusterClient, TGlideClient +from glide.shared.routes import AllNodes, SlotIdRoute, SlotKeyRoute, SlotType from tests.conftest import create_client from tests.utils.utils import ( diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 2c0d1cf64e..f81a2696bf 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -1,6 +1,6 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from glide.config import ( +from glide.shared.config import ( AdvancedGlideClientConfiguration, AdvancedGlideClusterClientConfiguration, BackoffStrategy, @@ -12,9 +12,9 @@ PeriodicChecksStatus, ReadFrom, ) -from glide.protobuf.connection_request_pb2 import ConnectionRequest -from glide.protobuf.connection_request_pb2 import ReadFrom as ProtobufReadFrom -from glide.protobuf.connection_request_pb2 import TlsMode +from .protobuf.connection_request_pb2 import ConnectionRequest +from .protobuf.connection_request_pb2 import ReadFrom as ProtobufReadFrom +from .protobuf.connection_request_pb2 import TlsMode def test_default_client_config(): diff --git a/python/tests/test_proto_coded.py b/python/tests/test_proto_coded.py index 96bc7bc0a9..f23f87b4d9 100644 --- a/python/tests/test_proto_coded.py +++ b/python/tests/test_proto_coded.py @@ -1,9 +1,9 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 import pytest -from glide.protobuf.command_request_pb2 import CommandRequest, RequestType -from glide.protobuf.response_pb2 import Response -from glide.protobuf_codec import PartialMessageException, ProtobufCodec +from .protobuf.command_request_pb2 import CommandRequest, RequestType +from .protobuf.response_pb2 import Response +from .protobuf_codec import PartialMessageException, ProtobufCodec class TestProtobufCodec: diff --git a/python/tests/test_pubsub.py b/python/tests/test_pubsub.py index e5fe6fc997..a82799b158 100644 --- a/python/tests/test_pubsub.py +++ b/python/tests/test_pubsub.py @@ -7,15 +7,15 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast import pytest -from glide.async_commands.core import CoreCommands -from glide.config import ( +from glide.shared.commands.core_options import PubSubMsg +from glide.shared.config import ( GlideClientConfiguration, GlideClusterClientConfiguration, ProtocolVersion, ) -from glide.constants import OK -from glide.exceptions import ConfigurationError -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient +from glide.shared.constants import OK +from glide.shared.exceptions import ConfigurationError +from glide.glide_async.python.glide import GlideClient, GlideClusterClient, TGlideClient from tests.conftest import create_client from tests.utils.utils import check_if_server_version_lt, get_random_string @@ -85,20 +85,20 @@ async def create_two_clients_with_pubsub( return client1, client2 -def decode_pubsub_msg(msg: Optional[CoreCommands.PubSubMsg]) -> CoreCommands.PubSubMsg: +def decode_pubsub_msg(msg: Optional[PubSubMsg]) -> PubSubMsg: if not msg: - return CoreCommands.PubSubMsg("", "", None) + return PubSubMsg("", "", None) string_msg = cast(bytes, msg.message).decode() string_channel = cast(bytes, msg.channel).decode() string_pattern = cast(bytes, msg.pattern).decode() if msg.pattern else None - decoded_msg = CoreCommands.PubSubMsg(string_msg, string_channel, string_pattern) + decoded_msg = PubSubMsg(string_msg, string_channel, string_pattern) return decoded_msg async def get_message_by_method( method: MethodTesting, client: TGlideClient, - messages: Optional[List[CoreCommands.PubSubMsg]] = None, + messages: Optional[List[PubSubMsg]] = None, index: Optional[int] = None, ): if method == MethodTesting.Async: @@ -150,8 +150,8 @@ def create_pubsub_subscription( ) -def new_message(msg: CoreCommands.PubSubMsg, context: Any): - received_messages: List[CoreCommands.PubSubMsg] = context +def new_message(msg: PubSubMsg, context: Any): + received_messages: List[PubSubMsg] = context received_messages.append(msg) @@ -225,7 +225,7 @@ async def test_pubsub_exact_happy_path( message = get_random_string(5) callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -347,7 +347,7 @@ async def test_pubsub_exact_happy_path_many_channels( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -496,7 +496,7 @@ async def test_sharded_pubsub( publish_response = 1 callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -636,7 +636,7 @@ async def test_sharded_pubsub_many_channels( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -721,7 +721,7 @@ async def test_pubsub_pattern( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -852,7 +852,7 @@ async def test_pubsub_pattern_many_channels( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages @@ -939,7 +939,7 @@ async def test_pubsub_combined_exact_and_pattern_one_client( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1059,7 +1059,7 @@ async def test_pubsub_combined_exact_and_pattern_multiple_clients( } callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1090,7 +1090,7 @@ async def test_pubsub_combined_exact_and_pattern_multiple_clients( ) ) - callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + callback_messages_pattern: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message context = callback_messages_pattern @@ -1217,7 +1217,7 @@ async def test_pubsub_combined_exact_pattern_and_sharded_one_client( publish_response = 1 callback, context = None, None - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1365,9 +1365,9 @@ async def test_pubsub_combined_exact_pattern_and_sharded_multi_client( publish_response = 1 callback, context = None, None - callback_messages_exact: List[CoreCommands.PubSubMsg] = [] - callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] - callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + callback_messages_exact: List[PubSubMsg] = [] + callback_messages_pattern: List[PubSubMsg] = [] + callback_messages_sharded: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1572,9 +1572,9 @@ async def test_pubsub_combined_different_channels_with_same_name( MESSAGE_SHARDED = get_random_string(5) callback, context = None, None - callback_messages_exact: List[CoreCommands.PubSubMsg] = [] - callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] - callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + callback_messages_exact: List[PubSubMsg] = [] + callback_messages_pattern: List[PubSubMsg] = [] + callback_messages_sharded: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1723,8 +1723,8 @@ async def test_pubsub_two_publishing_clients_same_name( MESSAGE_EXACT = get_random_string(10) MESSAGE_PATTERN = get_random_string(7) callback, context_exact, context_pattern = None, None, None - callback_messages_exact: List[CoreCommands.PubSubMsg] = [] - callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] + callback_messages_exact: List[PubSubMsg] = [] + callback_messages_pattern: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -1836,9 +1836,9 @@ async def test_pubsub_three_publishing_clients_same_name_with_sharded( None, None, ) - callback_messages_exact: List[CoreCommands.PubSubMsg] = [] - callback_messages_pattern: List[CoreCommands.PubSubMsg] = [] - callback_messages_sharded: List[CoreCommands.PubSubMsg] = [] + callback_messages_exact: List[PubSubMsg] = [] + callback_messages_pattern: List[PubSubMsg] = [] + callback_messages_sharded: List[PubSubMsg] = [] if method == MethodTesting.Callback: callback = new_message @@ -2124,7 +2124,7 @@ async def test_pubsub_exact_max_size_message_callback( channel = get_random_string(10) message = "0" * 12 * 1024 * 1024 - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] callback, context = new_message, callback_messages pub_sub = create_pubsub_subscription( @@ -2182,7 +2182,7 @@ async def test_pubsub_sharded_max_size_message_callback( channel = get_random_string(10) message = "0" * 512 * 1024 * 1024 - callback_messages: List[CoreCommands.PubSubMsg] = [] + callback_messages: List[PubSubMsg] = [] callback, context = new_message, callback_messages pub_sub = create_pubsub_subscription( @@ -2239,7 +2239,7 @@ async def test_pubsub_context_with_no_callback_raise_error( ): """Tests that when creating a PUBSUB client in callback method with context but no callback raises an error""" channel = get_random_string(5) - context: List[CoreCommands.PubSubMsg] = [] + context: List[PubSubMsg] = [] pub_sub_exact = create_pubsub_subscription( cluster_mode, {GlideClusterClientConfiguration.PubSubChannelModes.Exact: {channel}}, diff --git a/python/tests/test_read_from_strategy.py b/python/tests/test_read_from_strategy.py index e50a5ae769..486c20c4ea 100644 --- a/python/tests/test_read_from_strategy.py +++ b/python/tests/test_read_from_strategy.py @@ -4,11 +4,11 @@ from typing import Mapping, cast import pytest -from glide.async_commands.core import InfoSection -from glide.config import ProtocolVersion, ReadFrom -from glide.constants import OK -from glide.glide_client import GlideClusterClient -from glide.routes import AllNodes, SlotIdRoute, SlotType +from glide.shared.commands.core_options import InfoSection +from glide.shared.config import ProtocolVersion, ReadFrom +from glide.shared.constants import OK +from glide.glide_async.python.glide import GlideClusterClient +from glide.shared.routes import AllNodes, SlotIdRoute, SlotType from tests.conftest import create_client from tests.utils.utils import get_first_result diff --git a/python/tests/test_scan.py b/python/tests/test_scan.py index 977127bfc4..c5e1bded40 100644 --- a/python/tests/test_scan.py +++ b/python/tests/test_scan.py @@ -2,12 +2,12 @@ from typing import AsyncGenerator, List, cast import pytest -from glide import ByAddressRoute -from glide.async_commands.command_args import ObjectType -from glide.config import ProtocolVersion -from glide.exceptions import RequestError -from glide.glide import ClusterScanCursor -from glide.glide_client import GlideClient, GlideClusterClient +from glide.shared.routes import ByAddressRoute +from glide.shared.commands.command_args import ObjectType +from glide.shared.config import ProtocolVersion +from glide.shared.exceptions import RequestError +from glide.glide_async.python.glide.glide import ClusterScanCursor +from glide.glide_async.python.glide import GlideClient, GlideClusterClient from tests.conftest import create_client from tests.utils.cluster import ValkeyCluster diff --git a/python/tests/test_sync_client.py b/python/tests/test_sync_client.py new file mode 100644 index 0000000000..9291154d2e --- /dev/null +++ b/python/tests/test_sync_client.py @@ -0,0 +1,27 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +# mypy: disable_error_code="arg-type" + +from __future__ import annotations + +import pytest +from glide.shared.config import ProtocolVersion +from glide.shared.constants import OK +from glide.glide_sync.glide_sync import TGlideClient + +from tests.utils.utils import get_random_string + + +class TestGlideClients: + @pytest.mark.skip_if_version_below("6.2.0") + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + def test_sync_set_return_old_value(self, glide_sync_client: TGlideClient): + key = get_random_string(10) + value = get_random_string(10) + res = glide_sync_client.set(key, value) + assert res == OK + assert glide_sync_client.get(key) == value.encode() + new_value = get_random_string(10) + res = glide_sync_client.set(key, new_value, return_old_value=True) + assert res == value.encode() + assert glide_sync_client.get(key) == new_value.encode() diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py index 8d7e9b7aff..b75e978a41 100644 --- a/python/tests/test_utils.py +++ b/python/tests/test_utils.py @@ -1,6 +1,6 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -from glide.logger import Level, Logger +glide.glide_async.python.glide.logger import Level, Logger from tests.conftest import DEFAULT_TEST_LOG_LEVEL from tests.utils.utils import compare_maps diff --git a/python/tests/tests_server_modules/test_ft.py b/python/tests/tests_server_modules/test_ft.py index 7486f689b5..619029f14c 100644 --- a/python/tests/tests_server_modules/test_ft.py +++ b/python/tests/tests_server_modules/test_ft.py @@ -7,10 +7,10 @@ from typing import List, Mapping, Union, cast import pytest -from glide.async_commands.command_args import OrderBy -from glide.async_commands.server_modules import ft -from glide.async_commands.server_modules import glide_json as GlideJson -from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( +from glide.shared.commands.command_args import OrderBy +from glide.shared.commands.server_modules import ft +from glide.shared.commands.server_modules import glide_json as GlideJson +from glide.shared.commands.server_modules.ft_options.ft_aggregate_options import ( FtAggregateApply, FtAggregateGroupBy, FtAggregateOptions, @@ -18,7 +18,7 @@ FtAggregateSortBy, FtAggregateSortProperty, ) -from glide.async_commands.server_modules.ft_options.ft_create_options import ( +from glide.shared.commands.server_modules.ft_options.ft_create_options import ( DataType, DistanceMetricType, Field, @@ -32,17 +32,15 @@ VectorFieldAttributesHnsw, VectorType, ) -from glide.async_commands.server_modules.ft_options.ft_profile_options import ( - FtProfileOptions, -) -from glide.async_commands.server_modules.ft_options.ft_search_options import ( +from glide.shared.commands.server_modules.ft_options.ft_profile_options import FtProfileOptions +from glide.shared.commands.server_modules.ft_options.ft_search_options import ( FtSearchOptions, ReturnField, ) -from glide.config import ProtocolVersion -from glide.constants import OK, FtSearchResponse, TEncodable -from glide.exceptions import RequestError -from glide.glide_client import GlideClusterClient +from glide.shared.config import ProtocolVersion +from glide.shared.constants import OK, FtSearchResponse, TEncodable +from glide.shared.exceptions import RequestError +from glide.glide_async.python.glide import GlideClusterClient @pytest.mark.asyncio diff --git a/python/tests/tests_server_modules/test_json.py b/python/tests/tests_server_modules/test_json.py index 9af9b48f61..e38f64e143 100644 --- a/python/tests/tests_server_modules/test_json.py +++ b/python/tests/tests_server_modules/test_json.py @@ -7,19 +7,19 @@ from typing import List, Optional import pytest -from glide.async_commands.batch import ClusterBatch -from glide.async_commands.core import ConditionalChange -from glide.async_commands.server_modules import glide_json as json -from glide.async_commands.server_modules import json_batch -from glide.async_commands.server_modules.glide_json import ( +from glide.shared.commands.batch import ClusterBatch +from glide.shared.commands.core_options import ConditionalChange +from glide.shared.commands.server_modules import glide_json as json +from glide.shared.commands.server_modules import json_batch +from glide.shared.commands.server_modules.glide_json import ( JsonArrIndexOptions, JsonArrPopOptions, JsonGetOptions, ) -from glide.config import ProtocolVersion -from glide.constants import OK -from glide.exceptions import RequestError -from glide.glide_client import GlideClusterClient, TGlideClient +from glide.shared.config import ProtocolVersion +from glide.shared.constants import OK +from glide.shared.exceptions import RequestError +from glide.glide_async.python.glide import GlideClusterClient, TGlideClient from tests.test_async_client import get_random_string diff --git a/python/tests/utils/cluster.py b/python/tests/utils/cluster.py index 15e202780a..45487b1939 100644 --- a/python/tests/utils/cluster.py +++ b/python/tests/utils/cluster.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import List, Optional -from glide.config import NodeAddress +from glide.shared.config import NodeAddress SCRIPT_FILE = ( Path(__file__).parent.parent.parent.parent / "utils" / "cluster_manager.py" diff --git a/python/tests/utils/utils.py b/python/tests/utils/utils.py index 8912afd3c5..81dac92418 100644 --- a/python/tests/utils/utils.py +++ b/python/tests/utils/utils.py @@ -4,15 +4,16 @@ from typing import Any, Dict, List, Mapping, Optional, Set, TypeVar, Union, cast import pytest -from glide.async_commands.core import InfoSection -from glide.constants import ( +from glide.shared.commands.core_options import InfoSection +from glide.shared.constants import ( TClusterResponse, TFunctionListResponse, TFunctionStatsSingleNodeResponse, TResult, ) -from glide.glide_client import GlideClient, GlideClusterClient, TGlideClient -from glide.routes import AllNodes +from glide.glide_async.python.glide import GlideClient, GlideClusterClient, TGlideClient # TODO: change that to support both sync client +from glide.shared.routes import AllNodes +from glide.glide_sync.glide_sync import TGlideClient as TSyncGlideClient from packaging import version T = TypeVar("T") @@ -80,7 +81,6 @@ def get_random_string(length): async def check_if_server_version_lt(client: TGlideClient, min_version: str) -> bool: - # TODO: change to pytest fixture after sync client is implemented global version_str if not version_str: info = parse_info_response(await client.info([InfoSection.SERVER])) @@ -89,6 +89,13 @@ async def check_if_server_version_lt(client: TGlideClient, min_version: str) -> return version.parse(version_str) < version.parse(min_version) +def sync_check_if_server_version_lt(client: TSyncGlideClient, min_version: str) -> bool: + info = parse_info_response(client.info([InfoSection.SERVER])) + version_str = info.get("valkey_version") or info.get("redis_version") + assert version_str is not None, "Server version not found in INFO response" + return version.parse(version_str) < version.parse(min_version) + + def compare_maps( map1: Optional[ Union[ diff --git a/test_glide_meta.egg-info/PKG-INFO b/test_glide_meta.egg-info/PKG-INFO new file mode 100644 index 0000000000..dde34efb3c --- /dev/null +++ b/test_glide_meta.egg-info/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.4 +Name: test-glide-meta +Version: 0.1.0 +Summary: Meta client for test-glide-meta, combining sync and async clients +License: MIT +Provides-Extra: sync +Requires-Dist: test-glide-async; extra == "sync" +Provides-Extra: async +Requires-Dist: test-glide-sync; extra == "async" +Provides-Extra: full +Requires-Dist: test-glide-async; extra == "full" +Requires-Dist: test-glide-sync; extra == "full" diff --git a/test_glide_meta.egg-info/SOURCES.txt b/test_glide_meta.egg-info/SOURCES.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test_glide_meta.egg-info/dependency_links.txt b/test_glide_meta.egg-info/dependency_links.txt new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/test_glide_meta.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/test_glide_meta.egg-info/requires.txt b/test_glide_meta.egg-info/requires.txt new file mode 100644 index 0000000000..18a26bc2d2 --- /dev/null +++ b/test_glide_meta.egg-info/requires.txt @@ -0,0 +1,10 @@ + +[async] +test-glide-sync + +[full] +test-glide-async +test-glide-sync + +[sync] +test-glide-async diff --git a/test_glide_meta.egg-info/top_level.txt b/test_glide_meta.egg-info/top_level.txt new file mode 100644 index 0000000000..c0bf0135b0 --- /dev/null +++ b/test_glide_meta.egg-info/top_level.txt @@ -0,0 +1 @@ +test-glide-meta diff --git a/utils/release-candidate-testing/python/rc_test.py b/utils/release-candidate-testing/python/rc_test.py index 889abf94fc..e3160a13b1 100644 --- a/utils/release-candidate-testing/python/rc_test.py +++ b/utils/release-candidate-testing/python/rc_test.py @@ -8,7 +8,7 @@ SCRIPT_FILE = os.path.abspath(f"{__file__}/../../../cluster_manager.py") -from glide import ( +from shared import ( GlideClient, GlideClientConfiguration, GlideClusterClient,