From 1945b25e011e584f9d04c0b7bc5f46faab15038f Mon Sep 17 00:00:00 2001 From: Lakitna Date: Fri, 13 Mar 2026 15:56:21 +0000 Subject: [PATCH 01/11] Added tests --- test/integration/run-multi-value.robot | 155 +++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/integration/run-multi-value.robot diff --git a/test/integration/run-multi-value.robot b/test/integration/run-multi-value.robot new file mode 100644 index 0000000..060171e --- /dev/null +++ b/test/integration/run-multi-value.robot @@ -0,0 +1,155 @@ +*** Settings *** +Library Collections +Library Process +Library FakerLibrary +Library pabot.PabotLib +Library CacheLibrary + +# Clean up the data created by these tests +Suite Setup Run Only Once Cache Reset +Suite Teardown Run On Last Process Cache Reset + + +*** Variables *** +${ITERATIONS} 50 +@{SUPPORTED_PRIMITIVES} +... str +... bool +... int +... float + + +*** Test Cases *** +Retrieve one value from set at a time + ${input} = Evaluate list(range(10)) + Cache Store Collection set-data-${TEST_NAME} @{input} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${0} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${1} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${2} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${3} + +Sets and values used together + ${input} = Evaluate list(range(10)) + Cache Store Value val-data-${TEST_NAME} ${input} + Cache Store Collection set-data-${TEST_NAME} @{input} + + ${retrieved_val} = Cache Retrieve Value val-data-${TEST_NAME} + ${retrieved_set_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + + Lists Should Be Equal ${retrieved_val} ${input} + Should Be Equal As Integers ${retrieved_set_val} ${0} + +Sets can't be retrieved as value + ${input} = Evaluate list(range(10)) + Cache Store Collection set-data-${TEST_NAME} @{input} + + ${retrieved_val} = Cache Retrieve Value set-data-${TEST_NAME} + Should Be Equal ${retrieved_val} ${None} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal ${retrieved_val} ${0} + +Values can't be retrieved as set + ${input} = Evaluate list(range(10)) + Cache Store Value val-data-${TEST_NAME} ${input} + + ${retrieved_val} = Cache Retrieve Value val-data-${TEST_NAME} + Should Be Equal ${retrieved_val} ${input} + + ${retrieved_val} = Cache Retrieve Value From Collection val-data-${TEST_NAME} + Should Be Equal ${retrieved_val} ${None} + +Returns None when the set is empty + ${input} = Evaluate list(range(3)) + Cache Store Collection set-data-${TEST_NAME} @{input} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${0} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${1} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal As Integers ${retrieved_val} ${2} + + ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} + Should Be Equal ${retrieved_val} ${None} + +Returns None when the set does not exist + ${retrieved_val} = Cache Retrieve Value From Collection i-do-not-exist + Should Be Equal ${retrieved_val} ${None} + +Store and retrieve random string data + ${len} = FakerLibrary.Pyint min_value=1 max_value=100 + + ${value_set} = Create List + FOR ${i} IN RANGE ${ITERATIONS} + ${input} = FakerLibrary.Pystr min_chars=${len} max_chars=${len} + Append To List ${value_set} ${input} + END + Cache Store Collection random-strings @{value_set} + + FOR ${i} IN RANGE ${ITERATIONS} + ${retrieved} = Cache Retrieve Value From Collection random-strings pick=first remove_value=True + Should Be Equal ${retrieved} ${value_set}[${i}] + END + +Store and retrieve random int data + ${value_set} = Create List + FOR ${i} IN RANGE ${ITERATIONS} + ${input} = FakerLibrary.Pyint + Append To List ${value_set} ${input} + END + Cache Store Collection random-ints @{value_set} + + FOR ${i} IN RANGE ${ITERATIONS} + ${retrieved} = Cache Retrieve Value From Collection random-ints pick=first remove_value=True + Should Be Equal ${retrieved} ${value_set}[${i}] + END + +Store and retrieve random float data + ${value_set} = Create List + FOR ${i} IN RANGE ${ITERATIONS} + ${input} = FakerLibrary.Pyfloat + Append To List ${value_set} ${input} + END + Cache Store Collection random-floats @{value_set} + + FOR ${i} IN RANGE ${ITERATIONS} + ${retrieved} = Cache Retrieve Value From Collection random-floats pick=first remove_value=True + Should Be Equal ${retrieved} ${value_set}[${i}] + END + +Store and retrieve random dict sdata + ${value_set} = Create List + FOR ${i} IN RANGE ${ITERATIONS} + ${input} = FakerLibrary.Pydict value_types=${SUPPORTED_PRIMITIVES} + Append To List ${value_set} ${input} + END + Cache Store Collection random-dicts @{value_set} + + FOR ${i} IN RANGE ${ITERATIONS} + ${retrieved} = Cache Retrieve Value From Collection random-dicts pick=first remove_value=True + Should Be Equal ${retrieved} ${value_set}[${i}] + END + +Store and retrieve random list data + ${value_set} = Create List + FOR ${i} IN RANGE ${ITERATIONS} + ${input} = FakerLibrary.Pylist value_types=${SUPPORTED_PRIMITIVES} + Append To List ${value_set} ${input} + END + Cache Store Collection random-lists @{value_set} + + FOR ${i} IN RANGE ${ITERATIONS} + ${retrieved} = Cache Retrieve Value From Collection random-lists pick=first remove_value=True + Should Be Equal ${retrieved} ${value_set}[${i}] + END From 0e4bb10046df0a84322287ea6f03ce3de0e79d8c Mon Sep 17 00:00:00 2001 From: Lakitna Date: Fri, 13 Mar 2026 15:56:37 +0000 Subject: [PATCH 02/11] Added concept of cached collections --- src/CacheLibrary/CacheLibrary.py | 207 +++++++++++++++++++++++++++---- 1 file changed, 185 insertions(+), 22 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index ecd058a..a7ec49c 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -1,4 +1,5 @@ import json +import random from collections.abc import Generator from contextlib import contextmanager from datetime import datetime, timedelta @@ -15,6 +16,8 @@ CacheKey: TypeAlias = str CacheValue: TypeAlias = str | bool | int | float | list | dict +CacheValueType: TypeAlias = Literal["COLLECTION", "VALUE"] +CACHE_VALUE_TYPES: tuple[CacheValueType, ...] = ("COLLECTION", "VALUE") class CacheEntry(TypedDict): @@ -26,7 +29,7 @@ class CacheEntry(TypedDict): expires: str -CacheContents: TypeAlias = dict[CacheKey, CacheEntry] +CacheContents: TypeAlias = dict[CacheValueType, dict[CacheKey, CacheEntry]] KwName: TypeAlias = str KwArgs: TypeAlias = Any @@ -123,7 +126,7 @@ def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None: | ${session_token} = Cache Retrieve Value user-session """ - cache = self._open_cache_file() + cache = self._open_cache_file()["VALUE"] if key not in cache: return None @@ -135,6 +138,65 @@ def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None: return entry["value"] + @keyword + def cache_retrieve_value_from_collection( + self, + key: CacheKey, + pick: Literal["first", "last", "random"] = "first", + remove_value: bool = True, # noqa: FBT001, FBT002 + ) -> CacheValue | None: + """ + Retrieve a value from a cached collection. + + Will return a single value from a collection stored in the cache, or `None` if there is no + value. + + | `key` | Name of the collection | + | `pick=first` | How to pick a value from the collection | + | `remove_value=True` | Should the value be removed from the collection | + + = Examples = + + == Basic usage == + + Retrieve a value from a cached collection. + + | ${user} = Cache Retrieve Value From Collection user-accounts + + TODO: More examples + """ + cache = self._open_cache_file()["COLLECTION"] + + if key not in cache: + return None + + entry = cache[key] + if self._entry_is_expired(entry): + self.cache_remove_collection(key) + return None + + values = entry["value"] + if not isinstance(values, list) or len(values) == 0: + self.cache_remove_collection(key) + return None + + index = None + if pick == "first": + index = 0 + elif pick == "last": + index = -1 + elif pick == "random": + index = random.randint(0, len(values) - 1) # noqa: S311 + else: + msg = f"Unexpected pick '{pick}'. Expected one of 'first', 'last', or 'random'." + raise ValueError(msg) + + value = values[index] + if remove_value: + self._cache_remove_collection_value(key, index) + + return value + @keyword def cache_store_value( self, @@ -175,19 +237,89 @@ def cache_store_value( | Cache Store Value user-session ${session_token} expire_in_seconds=60 """ + self._store_cache_entry(key, value, "VALUE", expire_in_seconds) + + @keyword + def cache_store_collection( + self, + key: CacheKey, + *values: CacheValue, + expire_in_seconds: int | Literal["default"] = "default", + ): + """ + Store a collection of values in the cache. + + All values in the collection must be able to be stored in JSON. Supported values include + (but are not limited to): + + - String + - Integer + - Float + - Boolean + - Dictionary + - List + + | `key` | Name of the collection to be stored | + | `*values` | Values to be stored. Can be multiple. | + | `expire_in_seconds=default` | After how many seconds the full collection should expire | + + = Examples = + + == Basic usage == + + Store a collection of values in the cache + + | VAR @{users} = Henk Harry Herman + | Cache Store Collection usernames @{usernames} + + -------------------- + + == Control expiration == + + Store a collection of values in the cache and set them to expire in 1 minute. All values + will expire at the same time. + + | VAR @{users} = Henk Harry Herman + | Cache Store Collection usernames @{usernames} expire_in_seconds=60 + """ + self._store_cache_entry(key, list(values), "COLLECTION", expire_in_seconds) + + def _store_cache_entry( + self, + key: CacheKey, + value: CacheValue, + value_type: CacheValueType, + expire_in_seconds: int | Literal["default"], + ) -> None: if expire_in_seconds == "default": expire_in_seconds = self.default_expire_in_seconds + expires = (datetime.now() + timedelta(seconds=expire_in_seconds)).isoformat() + cache_entry: CacheEntry = { + "value": value, + "expires": expires, + } + with self._lock("cachelib-edit"): cache = self._open_cache_file() + cache[value_type][key] = cache_entry - expires = (datetime.now() + timedelta(seconds=expire_in_seconds)).isoformat() - cache_entry: CacheEntry = { - "value": value, - "expires": expires, - } + self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) + self._store_json_file(self.file_path, cache) - cache[key] = cache_entry + def _cache_remove_collection_value(self, key: CacheKey, index: int) -> None: + with self._lock("cachelib-edit"): + cache = self._open_cache_file() + set_cache = cache["COLLECTION"] + + if key not in set_cache: + return + + values = set_cache[key]["value"] + if not isinstance(values, list): + return + + values.pop(index) self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) self._store_json_file(self.file_path, cache) @@ -204,13 +336,37 @@ def cache_remove_value(self, key: CacheKey) -> None: | Cache Remove Value user-session """ + self._remove_cache_entry(key, "VALUE") + + @keyword + def cache_remove_collection(self, key: CacheKey) -> None: + """ + Remove a collection from the cache. + + Removes the entire collection, including all values. + + | `key` | Name of the stored collection | + + = Examples = + + Remove a collection from the cache + + | Cache Remove Collection test-users + """ + self._remove_cache_entry(key, "COLLECTION") + + def _remove_cache_entry( + self, + key: CacheKey, + value_type: CacheValueType, + ) -> None: with self._lock("cachelib-edit"): cache = self._open_cache_file() - if key not in cache: + if key not in cache[value_type]: return - del cache[key] + del cache[value_type][key] self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) self._store_json_file(self.file_path, cache) @@ -219,9 +375,10 @@ def cache_reset(self) -> None: """ Remove all values from the cache. """ + empty_cache = self._ensure_complete_cache({}) with self._lock("cachelib-edit"): - self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, {}) - self._store_json_file(self.file_path, {}) + self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, empty_cache) + self._store_json_file(self.file_path, empty_cache) @keyword def run_keyword_and_cache_output( @@ -300,6 +457,11 @@ def run_keyword_and_cache_output( self.cache_store_value(key, new_value, expire_in_seconds) return new_value + def _ensure_complete_cache(self, cache: CacheContents) -> CacheContents: + for value_type in CACHE_VALUE_TYPES: + cache.setdefault(value_type, {}) + return cache + def _open_cache_file(self) -> CacheContents: shared_cache = self.pabotlib.get_parallel_value_for_key(self.parallel_value_key) # If not set, `shared_cache` will be an empty string. @@ -315,19 +477,19 @@ def _open_cache_file(self) -> CacheContents: "There might be an issue with the caching mechanism.", ) - # Filter out expired entries - cache_contents: CacheContents = {} - for key, entry in cache_file_contents.items(): - if not self._entry_is_expired(entry): - cache_contents[key] = entry + cache_contents: CacheContents = self._ensure_complete_cache({}) + for value_type, contents in cache_file_contents.items(): + for key, entry in contents.items(): + if self._entry_is_expired(entry): + continue + cache_contents[value_type][key] = entry self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache_contents) - self._store_json_file(self.file_path, cache_contents) return cache_contents def _read_json_file(self, path: Path) -> CacheContents: - with self._lock(f"file-{path}"): + with self._lock(f"cachelib-file-{path}"): try: with path.open("r", encoding="utf8") as f: return json.load(f) @@ -335,12 +497,13 @@ def _read_json_file(self, path: Path) -> CacheContents: raise except Exception: # noqa: BLE001 # Reset/create the file + empty_cache = self._ensure_complete_cache({}) with path.open("w", encoding="utf8") as f: - f.write("{}") - return {} + json.dump(empty_cache, f) + return empty_cache def _store_json_file(self, path: Path, contents: CacheContents) -> None: - with self._lock(f"file-{path}"), path.open("w", encoding="utf8") as f: + with self._lock(f"cachelib-file-{path}"), path.open("w", encoding="utf8") as f: return json.dump(contents, f) def _entry_is_expired(self, entry: CacheEntry) -> bool: From c25133cf72ef1f038aa9545595dc4754219a68ca Mon Sep 17 00:00:00 2001 From: Lakitna Date: Fri, 13 Mar 2026 15:56:51 +0000 Subject: [PATCH 03/11] Fixed existing tests --- test/acceptance/run.robot | 10 ++++++---- test/acceptance/test/A01.robot | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/test/acceptance/run.robot b/test/acceptance/run.robot index bc3b0fc..b04ea26 100644 --- a/test/acceptance/run.robot +++ b/test/acceptance/run.robot @@ -8,6 +8,7 @@ Library FakerLibrary *** Variables *** ${DEFAULT_EXPIRES_IN_SECONDS} ${3600} +${EMPTY_CACHE} \{"COLLECTION": \{\}, "VALUE": \{\}\} *** Test Cases *** @@ -56,7 +57,7 @@ A04 Resets the cache file when fetching and the cache file is not json File Should Exist ${file_cache_path} ${contents} = Get File ${file_cache_path} - Should Be Equal ${contents} \{\} + Should Be Equal ${contents} ${EMPTY_CACHE} [Teardown] Remove File ${file_cache_path} @@ -71,7 +72,7 @@ A05 Removes expired values from the cache file File Should Exist ${file_cache_path} ${contents} = Get File ${file_cache_path} - Should Be Equal ${contents} \{\} + Should Be Equal ${contents} ${EMPTY_CACHE} [Teardown] Remove File ${file_cache_path} @@ -84,7 +85,7 @@ A06 Overwrites default expiration time during import ${contents} = Get File ${file_cache_path} ${cache_content} = Evaluate json.loads('${contents}') modules=json - ${expires_date} = Set Variable ${cache_content['foo']['expires']} + ${expires_date} = Set Variable ${cache_content['VALUE']['foo']['expires']} ${expires_date} = DateTime.Convert Date ${expires_date} ${now} = DateTime.Get Current Date ${expires_in} = DateTime.Subtract Date From Date ${expires_date} ${now} result_format=number @@ -113,7 +114,8 @@ Create Cache File With Content Set To Dictionary ${cache_entries} ${key}=${entry} END - ${cache_file_content} = Evaluate json.dumps(${cache_entries}) modules=json + ${cache} = Create Dictionary VALUE=${cache_entries} + ${cache_file_content} = Evaluate json.dumps(${cache}) modules=json Create File ${file_name} ${cache_file_content} Run Test File With Robot diff --git a/test/acceptance/test/A01.robot b/test/acceptance/test/A01.robot index ace0a9b..e34f725 100644 --- a/test/acceptance/test/A01.robot +++ b/test/acceptance/test/A01.robot @@ -1,7 +1,8 @@ *** Settings *** -Library CacheLibrary robocache-A01.json +Library CacheLibrary robocache-A01.json + *** Test Cases *** Fetches expected data from file - ${value} = Cache Retrieve Value some-string-value + ${value} = Cache Retrieve Value some-string-value Should Be Equal ${value} Lorum ipsum dolor sit amet conscuer From daf3c7d4b4d0ccd3484764cfbe932f584138163f Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sat, 14 Mar 2026 12:54:49 +0100 Subject: [PATCH 04/11] Added test on removing values --- test/integration/run-multi-value.robot | 102 +++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/test/integration/run-multi-value.robot b/test/integration/run-multi-value.robot index 060171e..67e51e0 100644 --- a/test/integration/run-multi-value.robot +++ b/test/integration/run-multi-value.robot @@ -11,12 +11,19 @@ Suite Teardown Run On Last Process Cache Reset *** Variables *** -${ITERATIONS} 50 +${ITERATIONS} 50 @{SUPPORTED_PRIMITIVES} -... str -... bool -... int -... float +... str +... bool +... int +... float +@{COMPLEX_COLLECTION_TYPES} +... str +... bool +... int +... float +... list +... dict *** Test Cases *** @@ -36,6 +43,62 @@ Retrieve one value from set at a time ${retrieved_val} = Cache Retrieve Value From Collection set-data-${TEST_NAME} Should Be Equal As Integers ${retrieved_val} ${3} +Remove value by index + ${value_set} = Generate Complex Collection size=${ITERATIONS} + + Cache Store Collection set-data-${TEST_NAME} @{value_set} + + ${bad_index} = Evaluate ${ITERATIONS} + 1 + Run Keyword And Expect Error + ... Could not remove value from collection. Index out of range. Index ${bad_index} does not exist in cache collection 'set-data-Remove value by index'. Expected index between 0 and ${{ ${ITERATIONS} - 1 }}. IndexError: pop index out of range + ... Cache Remove Value From Collection + ... set-data-${TEST_NAME} + ... index=${bad_index} + + FOR ${value} IN @{value_set} + ${retrieved} = Cache Retrieve Value From Collection + ... set-data-${TEST_NAME} + ... pick=first + ... remove_value=False + Should Be Equal ${retrieved} ${value} + + Cache Remove Value From Collection set-data-${TEST_NAME} index=0 + END + + # Should now be empty + ${retrieved} = Cache Retrieve Value From Collection + ... set-data-${TEST_NAME} + ... remove_value=False + Should Be Equal ${retrieved} ${None} + +Remove value by value + ${value_set} = Generate Complex Collection size=${ITERATIONS} + + Cache Store Collection set-data-${TEST_NAME} @{value_set} + + ${bad_value} = Set Variable abcdefghijklmnopqrstuvwxyz + Run Keyword And Expect Error + ... Could not remove value from collection. Value not in collection. Value '${bad_value}' does not exist in cache collection 'set-data-${TEST_NAME}'. ValueError: list.remove(x): x not in list + ... Cache Remove Value From Collection + ... set-data-${TEST_NAME} + ... value=${bad_value} + + FOR ${value} IN @{value_set} + ${retrieved} = Cache Retrieve Value From Collection + ... set-data-${TEST_NAME} + ... pick=first + ... remove_value=False + Should Be Equal ${retrieved} ${value} + + Cache Remove Value From Collection set-data-${TEST_NAME} value=${retrieved} + END + + # Should now be empty + ${retrieved} = Cache Retrieve Value From Collection + ... set-data-${TEST_NAME} + ... remove_value=False + Should Be Equal ${retrieved} ${None} + Sets and values used together ${input} = Evaluate list(range(10)) Cache Store Value val-data-${TEST_NAME} ${input} @@ -153,3 +216,32 @@ Store and retrieve random list data ${retrieved} = Cache Retrieve Value From Collection random-lists pick=first remove_value=True Should Be Equal ${retrieved} ${value_set}[${i}] END + + +*** Keywords *** +Generate Complex Collection + [Arguments] ${size} ${types}=${COMPLEX_COLLECTION_TYPES} + ${collection} = Create List + ${types} = FakerLibrary.Random Elements ${types} length=${size} + + FOR ${type} IN @{types} + IF $type == 'str' + ${val} = FakerLibrary.Pystr + ELSE IF $type == 'int' + ${val} = FakerLibrary.Pyint + ELSE IF $type == 'float' + ${val} = FakerLibrary.Pyfloat + ELSE IF $type == 'bool' + ${val} = FakerLibrary.Pybool + ELSE IF $type == 'list' + ${val} = FakerLibrary.Pylist value_types=${SUPPORTED_PRIMITIVES} + ELSE IF $type == 'dict' + ${val} = FakerLibrary.Pydict value_types=${SUPPORTED_PRIMITIVES} + ELSE + Fail Unsupported type '${type}' + END + + Append To List ${collection} ${val} + END + + RETURN ${collection} From 69e99780aaaf9edc125a3165082800a878cacbf1 Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sat, 14 Mar 2026 12:55:35 +0100 Subject: [PATCH 05/11] Made removing collection values a keyword. Updated docstrings. Added kw tags --- src/CacheLibrary/CacheLibrary.py | 153 +++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 37 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index a7ec49c..c2f09a0 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -109,7 +109,7 @@ def __init__( self.file_size_warning_bytes = file_size_warning_bytes self.default_expire_in_seconds = default_expire_in_seconds - @keyword + @keyword(tags=["value"]) def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None: """ Retrieve a value from the cache. @@ -124,7 +124,7 @@ def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None: Retrieve a value from the cache - | ${session_token} = Cache Retrieve Value user-session + | ${session_token} = Cache Retrieve Value user-session """ cache = self._open_cache_file()["VALUE"] @@ -138,7 +138,7 @@ def cache_retrieve_value(self, key: CacheKey) -> CacheValue | None: return entry["value"] - @keyword + @keyword(tags=["collection"]) def cache_retrieve_value_from_collection( self, key: CacheKey, @@ -151,20 +151,27 @@ def cache_retrieve_value_from_collection( Will return a single value from a collection stored in the cache, or `None` if there is no value. - | `key` | Name of the collection | - | `pick=first` | How to pick a value from the collection | + | `key` | Name of the collection | + | `pick=first` | How to pick a value from the collection | | `remove_value=True` | Should the value be removed from the collection | = Examples = == Basic usage == - Retrieve a value from a cached collection. + Retrieve the first value from a cached collection. - | ${user} = Cache Retrieve Value From Collection user-accounts + | ${user} = Cache Retrieve Value From Collection user-accounts - TODO: More examples - """ + -------------------- + + == Pick random value == + + Retrieve a random value from a cached collection. Don't remove the value from the + collection. + + | ${user} = Cache Retrieve Value From Collection user-accounts pick=random remove_value=${False} + """ # noqa: E501 cache = self._open_cache_file()["COLLECTION"] if key not in cache: @@ -193,11 +200,11 @@ def cache_retrieve_value_from_collection( value = values[index] if remove_value: - self._cache_remove_collection_value(key, index) + self.cache_remove_value_from_collection(key, index=index) return value - @keyword + @keyword(tags=["value"]) def cache_store_value( self, key: CacheKey, @@ -227,7 +234,7 @@ def cache_store_value( Store a value in the cache - | Cache Store Value user-session ${session_token} + | Cache Store Value user-session ${session_token} -------------------- @@ -235,11 +242,11 @@ def cache_store_value( Store a value in the cache and set it to expire in 1 minute - | Cache Store Value user-session ${session_token} expire_in_seconds=60 + | Cache Store Value user-session ${session_token} expire_in_seconds=60 """ self._store_cache_entry(key, value, "VALUE", expire_in_seconds) - @keyword + @keyword(tags=["collection"]) def cache_store_collection( self, key: CacheKey, @@ -269,8 +276,8 @@ def cache_store_collection( Store a collection of values in the cache - | VAR @{users} = Henk Harry Herman - | Cache Store Collection usernames @{usernames} + | VAR @{users} = Henk Harry Herman + | Cache Store Collection usernames @{usernames} -------------------- @@ -279,8 +286,8 @@ def cache_store_collection( Store a collection of values in the cache and set them to expire in 1 minute. All values will expire at the same time. - | VAR @{users} = Henk Harry Herman - | Cache Store Collection usernames @{usernames} expire_in_seconds=60 + | VAR @{users} = Henk Harry Herman + | Cache Store Collection usernames @{usernames} expire_in_seconds=60 """ self._store_cache_entry(key, list(values), "COLLECTION", expire_in_seconds) @@ -307,23 +314,95 @@ def _store_cache_entry( self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) self._store_json_file(self.file_path, cache) - def _cache_remove_collection_value(self, key: CacheKey, index: int) -> None: + @keyword(tags=["collection"]) + def cache_remove_value_from_collection( + self, + key: CacheKey, + *, + index: int | None = None, + value: CacheValue | None = None, + ) -> None: + """ + Remove a value from a cached collection. + + | `key` | Name of the stored collection | + | `index` | Index of the value to be removed | + | `value` | Exact value to be removed | + + = Examples = + + == Remove with index == + + Remove a value from a cached collection using index. + + | Cache Remove Value From Collection user-sessions index=3 + + -------------------- + + == Remove with value == + + Remove a value from a cached collection using the value. Must be the exact value. + + | ${session} = Cache Retrieve Value From Collection user-sessions + | Cache Remove Value From Collection user-sessions value=${session} + """ with self._lock("cachelib-edit"): cache = self._open_cache_file() - set_cache = cache["COLLECTION"] + collection_cache = cache["COLLECTION"] - if key not in set_cache: + if key not in collection_cache: return - values = set_cache[key]["value"] + values = collection_cache[key]["value"] if not isinstance(values, list): return - values.pop(index) + values = self._remove_value_from_collection(key, values, index=index, value=value) + self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) self._store_json_file(self.file_path, cache) - @keyword + def _remove_value_from_collection( + self, + col_name: CacheKey, + col_values: list[CacheValue], + *, + index: int | None = None, + value: CacheValue | None = None, + ) -> list[CacheValue]: + if index is not None and value is not None: + msg = f"Got both index and value" + raise ValueError(msg) + + if index is not None: + try: + col_values.pop(index) + return col_values + except IndexError as e: + msg = ( + "Could not remove value from collection. Index out of range. " + f"Index {index} does not exist in cache collection '{col_name}'. " + f"Expected index between 0 and {len(col_values) - 1}. " + f"{type(e).__name__}: {e}" + ) + raise AssertionError(msg) + + if value is not None: + try: + col_values.remove(value) + return col_values + except ValueError as e: + msg = ( + "Could not remove value from collection. Value not in collection. " + f"Value '{value}' does not exist in cache collection '{col_name}'. " + f"{type(e).__name__}: {e}" + ) + raise AssertionError(msg) + + msg = f"No index, no value" + raise ValueError(msg) + + @keyword(tags=["value"]) def cache_remove_value(self, key: CacheKey) -> None: """ Remove a value from the cache. @@ -338,7 +417,7 @@ def cache_remove_value(self, key: CacheKey) -> None: """ self._remove_cache_entry(key, "VALUE") - @keyword + @keyword(tags=["collection"]) def cache_remove_collection(self, key: CacheKey) -> None: """ Remove a collection from the cache. @@ -380,7 +459,7 @@ def cache_reset(self) -> None: self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, empty_cache) self._store_json_file(self.file_path, empty_cache) - @keyword + @keyword(tags=["value"]) def run_keyword_and_cache_output( self, keyword: KwName, @@ -397,8 +476,8 @@ def run_keyword_and_cache_output( to create incorrect caching behavior. You can easily create two keyword calls that functionally do the same thing, but are considered different for caching purposes. - | `keyword` | The keyword that to be run | - | `*args` | Arguments send to the keyword | + | `keyword` | The keyword that to be run | + | `*args` | Arguments send to the keyword | | `expire_in_seconds=default` | After how many seconds the value should expire | = Examples = @@ -433,16 +512,16 @@ def run_keyword_and_cache_output( Using the caching in a wrapper keyword makes things easier to manage and makes it harder to create incorrect caching behaviour. - | Do A Thing - | [Arguments] ${a} ${b} ${c} - | ${result} = Run Keyword And Cache Output Do The Actual Thing ${a} ${b} ${c} - | RETURN ${result} + | Do A Thing + | [Arguments] ${a} ${b} ${c} + | ${result} = Run Keyword And Cache Output Do The Actual Thing ${a} ${b} ${c} + | RETURN ${result} | - | Do The Actual Thing - | [Arguments] ${a} ${b} ${c} - | [Tags] robot:private - | # Do something - | RETURN ${output} + | Do The Actual Thing + | [Arguments] ${a} ${b} ${c} + | [Tags] robot:private + | # Do something + | RETURN ${output} """ # noqa: E501 key = "kw-" + keyword.lower().replace(" ", "_") + "-" + "::".join([str(a) for a in args]) cached_value = self.cache_retrieve_value(key) From 88f070b70b1583c1ceb91a419c9dc6baa98a17ba Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sat, 14 Mar 2026 13:00:21 +0100 Subject: [PATCH 06/11] Updated example --- src/CacheLibrary/CacheLibrary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index c2f09a0..542062d 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -343,7 +343,7 @@ def cache_remove_value_from_collection( Remove a value from a cached collection using the value. Must be the exact value. - | ${session} = Cache Retrieve Value From Collection user-sessions + | ${session} = Cache Retrieve Value From Collection user-sessions remove_value=${False} | Cache Remove Value From Collection user-sessions value=${session} """ with self._lock("cachelib-edit"): From fa1a6bee59b5d73870c72238bce715838e732d5f Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sat, 14 Mar 2026 13:08:18 +0100 Subject: [PATCH 07/11] Fixed lint issues --- src/CacheLibrary/CacheLibrary.py | 16 +++++++++------- test/integration/run-multi-value.robot | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index 542062d..826ff4a 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -345,7 +345,7 @@ def cache_remove_value_from_collection( | ${session} = Cache Retrieve Value From Collection user-sessions remove_value=${False} | Cache Remove Value From Collection user-sessions value=${session} - """ + """ # noqa: E501 with self._lock("cachelib-edit"): cache = self._open_cache_file() collection_cache = cache["COLLECTION"] @@ -371,13 +371,12 @@ def _remove_value_from_collection( value: CacheValue | None = None, ) -> list[CacheValue]: if index is not None and value is not None: - msg = f"Got both index and value" + msg = "Got both index and value. Pick one. Can't use both at the same time." raise ValueError(msg) if index is not None: try: col_values.pop(index) - return col_values except IndexError as e: msg = ( "Could not remove value from collection. Index out of range. " @@ -385,21 +384,24 @@ def _remove_value_from_collection( f"Expected index between 0 and {len(col_values) - 1}. " f"{type(e).__name__}: {e}" ) - raise AssertionError(msg) + raise AssertionError(msg) from e + else: + return col_values if value is not None: try: col_values.remove(value) - return col_values except ValueError as e: msg = ( "Could not remove value from collection. Value not in collection. " f"Value '{value}' does not exist in cache collection '{col_name}'. " f"{type(e).__name__}: {e}" ) - raise AssertionError(msg) + raise AssertionError(msg) from e + else: + return col_values - msg = f"No index, no value" + msg = "No index and no value. I don't know what value to remove cached collection." raise ValueError(msg) @keyword(tags=["value"]) diff --git a/test/integration/run-multi-value.robot b/test/integration/run-multi-value.robot index 67e51e0..1a1b4aa 100644 --- a/test/integration/run-multi-value.robot +++ b/test/integration/run-multi-value.robot @@ -191,7 +191,7 @@ Store and retrieve random float data Should Be Equal ${retrieved} ${value_set}[${i}] END -Store and retrieve random dict sdata +Store and retrieve random dict data ${value_set} = Create List FOR ${i} IN RANGE ${ITERATIONS} ${input} = FakerLibrary.Pydict value_types=${SUPPORTED_PRIMITIVES} From 94ee60708a60f1e417865051631e401a2e8760ea Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sat, 14 Mar 2026 13:10:47 +0100 Subject: [PATCH 08/11] Fixed tests in robot 5 --- test/integration/run-multi-value.robot | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/integration/run-multi-value.robot b/test/integration/run-multi-value.robot index 1a1b4aa..3335f92 100644 --- a/test/integration/run-multi-value.robot +++ b/test/integration/run-multi-value.robot @@ -225,17 +225,17 @@ Generate Complex Collection ${types} = FakerLibrary.Random Elements ${types} length=${size} FOR ${type} IN @{types} - IF $type == 'str' + IF '${type}' == 'str' ${val} = FakerLibrary.Pystr - ELSE IF $type == 'int' + ELSE IF '${type}' == 'int' ${val} = FakerLibrary.Pyint - ELSE IF $type == 'float' + ELSE IF '${type}' == 'float' ${val} = FakerLibrary.Pyfloat - ELSE IF $type == 'bool' + ELSE IF '${type}' == 'bool' ${val} = FakerLibrary.Pybool - ELSE IF $type == 'list' + ELSE IF '${type}' == 'list' ${val} = FakerLibrary.Pylist value_types=${SUPPORTED_PRIMITIVES} - ELSE IF $type == 'dict' + ELSE IF '${type}' == 'dict' ${val} = FakerLibrary.Pydict value_types=${SUPPORTED_PRIMITIVES} ELSE Fail Unsupported type '${type}' From cc943f2f9638c8a05dd0364e91d6263603828c5b Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sun, 15 Mar 2026 13:21:58 +0100 Subject: [PATCH 09/11] Added basic store logging --- src/CacheLibrary/CacheLibrary.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index 826ff4a..a779bfd 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -244,7 +244,8 @@ def cache_store_value( | Cache Store Value user-session ${session_token} expire_in_seconds=60 """ - self._store_cache_entry(key, value, "VALUE", expire_in_seconds) + entry = self._store_cache_entry(key, value, "VALUE", expire_in_seconds) + logger.info(f"Stored value for '{key}'. Expires {entry['expires']}") @keyword(tags=["collection"]) def cache_store_collection( @@ -289,7 +290,10 @@ def cache_store_collection( | VAR @{users} = Henk Harry Herman | Cache Store Collection usernames @{usernames} expire_in_seconds=60 """ - self._store_cache_entry(key, list(values), "COLLECTION", expire_in_seconds) + entry = self._store_cache_entry(key, list(values), "COLLECTION", expire_in_seconds) + logger.info( + f"Stored collection for '{key}' with {len(values)} values. Expires {entry['expires']}", + ) def _store_cache_entry( self, @@ -297,7 +301,7 @@ def _store_cache_entry( value: CacheValue, value_type: CacheValueType, expire_in_seconds: int | Literal["default"], - ) -> None: + ) -> CacheEntry: if expire_in_seconds == "default": expire_in_seconds = self.default_expire_in_seconds @@ -314,6 +318,8 @@ def _store_cache_entry( self.pabotlib.set_parallel_value_for_key(self.parallel_value_key, cache) self._store_json_file(self.file_path, cache) + return cache_entry + @keyword(tags=["collection"]) def cache_remove_value_from_collection( self, From 9ce8783aa67e937c4a8c1db6c8152beed265d6eb Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sun, 15 Mar 2026 13:22:06 +0100 Subject: [PATCH 10/11] Improved docstring --- src/CacheLibrary/CacheLibrary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index a779bfd..50fe56c 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -151,9 +151,9 @@ def cache_retrieve_value_from_collection( Will return a single value from a collection stored in the cache, or `None` if there is no value. - | `key` | Name of the collection | - | `pick=first` | How to pick a value from the collection | - | `remove_value=True` | Should the value be removed from the collection | + | `key` | Name of the collection | + | `pick=first` | How to pick a value from the collection. Can be 'first', 'last', or 'random' | + | `remove_value=True` | Should the value be removed from the collection | = Examples = From bdde3e446095f143c064c66a341e0b1f1d0fada3 Mon Sep 17 00:00:00 2001 From: Lakitna Date: Sun, 15 Mar 2026 14:07:01 +0100 Subject: [PATCH 11/11] Self review comments --- src/CacheLibrary/CacheLibrary.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index 50fe56c..72538e4 100644 --- a/src/CacheLibrary/CacheLibrary.py +++ b/src/CacheLibrary/CacheLibrary.py @@ -277,7 +277,7 @@ def cache_store_collection( Store a collection of values in the cache - | VAR @{users} = Henk Harry Herman + | VAR @{users} = Alice Bob Pekka Miikka | Cache Store Collection usernames @{usernames} -------------------- @@ -287,7 +287,7 @@ def cache_store_collection( Store a collection of values in the cache and set them to expire in 1 minute. All values will expire at the same time. - | VAR @{users} = Henk Harry Herman + | VAR @{users} = Alice Bob Pekka Miikka | Cache Store Collection usernames @{usernames} expire_in_seconds=60 """ entry = self._store_cache_entry(key, list(values), "COLLECTION", expire_in_seconds) @@ -331,6 +331,8 @@ def cache_remove_value_from_collection( """ Remove a value from a cached collection. + Requires `index` or `value`, but not both. + | `key` | Name of the stored collection | | `index` | Index of the value to be removed | | `value` | Exact value to be removed |