diff --git a/src/CacheLibrary/CacheLibrary.py b/src/CacheLibrary/CacheLibrary.py index ecd058a..72538e4 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 @@ -106,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. @@ -121,9 +124,9 @@ 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() + cache = self._open_cache_file()["VALUE"] if key not in cache: return None @@ -135,7 +138,73 @@ 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, + 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. Can be 'first', 'last', or 'random' | + | `remove_value=True` | Should the value be removed from the collection | + + = Examples = + + == Basic usage == + + Retrieve the first value from a cached collection. + + | ${user} = Cache Retrieve Value From Collection user-accounts + + -------------------- + + == 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: + 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_value_from_collection(key, index=index) + + return value + + @keyword(tags=["value"]) def cache_store_value( self, key: CacheKey, @@ -165,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} -------------------- @@ -173,25 +242,177 @@ 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 + """ + 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( + 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} = Alice Bob Pekka Miikka + | 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} = 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) + logger.info( + f"Stored collection for '{key}' with {len(values)} values. Expires {entry['expires']}", + ) + + def _store_cache_entry( + self, + key: CacheKey, + value: CacheValue, + value_type: CacheValueType, + expire_in_seconds: int | Literal["default"], + ) -> CacheEntry: 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 + + 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, + key: CacheKey, + *, + index: int | None = None, + value: CacheValue | None = None, + ) -> None: + """ + 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 | + + = 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 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"] + + if key not in collection_cache: + return + + values = collection_cache[key]["value"] + if not isinstance(values, list): + return - expires = (datetime.now() + timedelta(seconds=expire_in_seconds)).isoformat() - cache_entry: CacheEntry = { - "value": value, - "expires": expires, - } + values = self._remove_value_from_collection(key, values, index=index, value=value) - cache[key] = cache_entry 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 = "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) + 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) from e + else: + return col_values + + if value is not None: + try: + col_values.remove(value) + 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) from e + else: + return col_values + + msg = "No index and no value. I don't know what value to remove cached collection." + raise ValueError(msg) + + @keyword(tags=["value"]) def cache_remove_value(self, key: CacheKey) -> None: """ Remove a value from the cache. @@ -204,13 +425,37 @@ def cache_remove_value(self, key: CacheKey) -> None: | Cache Remove Value user-session """ + self._remove_cache_entry(key, "VALUE") + + @keyword(tags=["collection"]) + 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,11 +464,12 @@ 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 + @keyword(tags=["value"]) def run_keyword_and_cache_output( self, keyword: KwName, @@ -240,8 +486,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 = @@ -276,16 +522,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) @@ -300,6 +546,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 +566,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 +586,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: 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 diff --git a/test/integration/run-multi-value.robot b/test/integration/run-multi-value.robot new file mode 100644 index 0000000..3335f92 --- /dev/null +++ b/test/integration/run-multi-value.robot @@ -0,0 +1,247 @@ +*** 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 +@{COMPLEX_COLLECTION_TYPES} +... str +... bool +... int +... float +... list +... dict + + +*** 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} + +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} + 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 data + ${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 + + +*** 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}