We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use literal_eval for parsing configuration values
## Current behavior
*Please describe how the feature works today*
Currently, Prefect takes a [hardcoded stab](https://github.com/PrefectHQ/prefect/blob/master/src/prefect/configuration.py#L37) at converting strings to Python objects. We could replace this function with `ast.literal_eval()` and instantly gain a more flexible cast, including the ability to natively handle lists and dicts. 



## Proposed behavior
*Please describe your proposed change to the current behavior*

Currently, setting `PREFECT__X__Y="['a', 'b', 3]"` results in `prefect.config.x.y == "['a', 'b', 3]"`. This means some Prefect objects (thinking of environments, but also including the config itself) must re-parse config options. This is after we already make an attempt to coerce some values to known types. Replacing this with `ast.literal_eval()` would result in `prefect.config.x.y == ['a', 'b', 3]`.


## Example
*Please give an example of how the enhancement would be useful*


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/prefect/configuration.py`
Content:
```
1 import datetime
2 import os
3 import re
4 from typing import Optional, Union, cast
5 
6 import toml
7 from box import Box
8 
9 from prefect.utilities import collections
10 
11 DEFAULT_CONFIG = os.path.join(os.path.dirname(__file__), "config.toml")
12 USER_CONFIG = os.getenv("PREFECT__USER_CONFIG_PATH", "~/.prefect/config.toml")
13 BACKEND_CONFIG = os.getenv("PREFECT__BACKEND_CONFIG_PATH", "~/.prefect/backend.toml")
14 ENV_VAR_PREFIX = "PREFECT"
15 INTERPOLATION_REGEX = re.compile(r"\${(.[^${}]*)}")
16 
17 
18 class Config(Box):
19     """
20     A config is a Box subclass
21     """
22 
23     def copy(self) -> "Config":
24         """
25         Create a recursive copy of the config. Each level of the Config is a new Config object, so
26         modifying keys won't affect the original Config object. However, values are not
27         deep-copied, and mutations can affect the original.
28         """
29         new_config = Config()
30         for key, value in self.items():
31             if isinstance(value, Config):
32                 value = value.copy()
33             new_config[key] = value
34         return new_config
35 
36 
37 def string_to_type(val: str) -> Union[bool, int, float, str]:
38     """
39     Helper function for transforming string env var values into typed values.
40 
41     Maps:
42         - "true" (any capitalization) to `True`
43         - "false" (any capitalization) to `False`
44         - integers to `int`
45         - floats to `float`
46 
47     Arguments:
48         - val (str): the string value of an environment variable
49 
50     Returns:
51         Union[bool, int, float, str]: the type-cast env var value
52     """
53 
54     # bool
55     if val.upper() == "TRUE":
56         return True
57     elif val.upper() == "FALSE":
58         return False
59 
60     # int
61     try:
62         val_as_int = int(val)
63         if str(val_as_int) == val:
64             return val_as_int
65     except Exception:
66         pass
67 
68     # float
69     try:
70         val_as_float = float(val)
71         if str(val_as_float) == val:
72             return val_as_float
73     except Exception:
74         pass
75 
76     # return string value
77     return val
78 
79 
80 def interpolate_env_vars(env_var: str) -> Optional[Union[bool, int, float, str]]:
81     """
82     Expands (potentially nested) env vars by repeatedly applying
83     `expandvars` and `expanduser` until interpolation stops having
84     any effect.
85     """
86     if not env_var or not isinstance(env_var, str):
87         return env_var
88 
89     counter = 0
90 
91     while counter < 10:
92         interpolated = os.path.expanduser(os.path.expandvars(str(env_var)))
93         if interpolated == env_var:
94             # if a change was made, apply string-to-type casts; otherwise leave alone
95             # this is because we don't want to override TOML type-casting if this function
96             # is applied to a non-interpolated value
97             if counter > 1:
98                 interpolated = string_to_type(interpolated)  # type: ignore
99             return interpolated
100         else:
101             env_var = interpolated
102         counter += 1
103 
104     return None
105 
106 
107 def create_user_config(dest_path: str, source_path: str = DEFAULT_CONFIG) -> None:
108     """
109     Copies the default configuration to a user-customizable file at `dest_path`
110     """
111     dest_path = cast(str, interpolate_env_vars(dest_path))
112     if os.path.isfile(dest_path):
113         raise ValueError("File already exists: {}".format(dest_path))
114     os.makedirs(os.path.dirname(dest_path), exist_ok=True)
115 
116     with open(dest_path, "w") as dest:
117         with open(source_path, "r") as source:
118             dest.write(source.read())
119 
120 
121 # Process Config -------------------------------------------------------------
122 
123 
124 def process_task_defaults(config: Config) -> Config:
125     """
126     Converts task defaults from basic types to Python objects like timedeltas
127 
128     Args:
129         - config (Config): the configuration to modify
130     """
131     # make sure defaults exists
132     defaults = config.setdefault("tasks", {}).setdefault("defaults", {})
133 
134     # max_retries defaults to 0 if not set, False, or None
135     if not defaults.setdefault("max_retries", 0):
136         defaults.max_retries = 0
137     defaults.max_retries = defaults.get("max_retries", 0) or 0
138 
139     # retry_delay defaults to None if not set - also check for False because TOML has no NULL
140     if defaults.setdefault("retry_delay", False) is False:
141         defaults.retry_delay = None
142     elif isinstance(defaults.retry_delay, int):
143         defaults.retry_delay = datetime.timedelta(seconds=defaults.retry_delay)
144 
145     # timeout defaults to None if not set - also check for False because TOML has no NULL
146     if defaults.setdefault("timeout", False) is False:
147         defaults.timeout = None
148     elif isinstance(defaults.timeout, int):
149         defaults.timeout = datetime.timedelta(seconds=defaults.timeout)
150 
151     return config
152 
153 
154 # Validation ------------------------------------------------------------------
155 
156 
157 def validate_config(config: Config) -> None:
158     """
159     Validates that the configuration file is valid.
160         - keys do not shadow Config methods
161 
162     Note that this is performed when the config is first loaded, but not after.
163     """
164 
165     def check_valid_keys(config: Config) -> None:
166         """
167         Recursively check that keys do not shadow methods of the Config object
168         """
169         invalid_keys = dir(Config)
170         for k, v in config.items():
171             if k in invalid_keys:
172                 raise ValueError('Invalid config key: "{}"'.format(k))
173             if isinstance(v, Config):
174                 check_valid_keys(v)
175 
176     check_valid_keys(config)
177 
178 
179 # Load configuration ----------------------------------------------------------
180 
181 
182 def load_toml(path: str) -> dict:
183     """
184     Loads a config dictionary from TOML
185     """
186     return {
187         key: value
188         for key, value in toml.load(cast(str, interpolate_env_vars(path))).items()
189     }
190 
191 
192 def interpolate_config(config: dict, env_var_prefix: str = None) -> Config:
193     """
194     Processes a config dictionary, such as the one loaded from `load_toml`.
195     """
196 
197     # toml supports nested dicts, so we work with a flattened representation to do any
198     # requested interpolation
199     flat_config = collections.dict_to_flatdict(config)
200 
201     # --------------------- Interpolate env vars -----------------------
202     # check if any env var sets a configuration value with the format:
203     #     [ENV_VAR_PREFIX]__[Section]__[Optional Sub-Sections...]__[Key] = Value
204     # and if it does, add it to the config file.
205 
206     if env_var_prefix:
207 
208         for env_var, env_var_value in os.environ.items():
209             if env_var.startswith(env_var_prefix + "__"):
210 
211                 # strip the prefix off the env var
212                 env_var_option = env_var[len(env_var_prefix + "__") :]
213 
214                 # make sure the resulting env var has at least one delimitied section and key
215                 if "__" not in env_var:
216                     continue
217 
218                 # env vars with escaped characters are interpreted as literal "\", which
219                 # Python helpfully escapes with a second "\". This step makes sure that
220                 # escaped characters are properly interpreted.
221                 value = cast(str, env_var_value.encode().decode("unicode_escape"))
222 
223                 # place the env var in the flat config as a compound key
224                 if env_var_option.upper().startswith("CONTEXT__SECRETS"):
225                     formatted_option = env_var_option.split("__")
226                     formatted_option[:-1] = [
227                         val.lower() for val in formatted_option[:-1]
228                     ]
229                     config_option = collections.CompoundKey(formatted_option)
230                 else:
231                     config_option = collections.CompoundKey(
232                         env_var_option.lower().split("__")
233                     )
234 
235                 flat_config[config_option] = string_to_type(
236                     cast(str, interpolate_env_vars(value))
237                 )
238 
239     # interpolate any env vars referenced
240     for k, v in list(flat_config.items()):
241         flat_config[k] = interpolate_env_vars(v)
242 
243     # --------------------- Interpolate other config keys -----------------
244     # TOML doesn't support references to other keys... but we do!
245     # This has the potential to lead to nasty recursions, so we check at most 10 times.
246     # we use a set called "keys_to_check" to track only the ones of interest, so we aren't
247     # checking every key every time.
248 
249     keys_to_check = set(flat_config.keys())
250 
251     for _ in range(10):
252 
253         # iterate over every key and value to check if the value uses interpolation
254         for k in list(keys_to_check):
255 
256             # if the value isn't a string, it can't be a reference, so we exit
257             if not isinstance(flat_config[k], str):
258                 keys_to_check.remove(k)
259                 continue
260 
261             # see if the ${...} syntax was used in the value and exit if it wasn't
262             match = INTERPOLATION_REGEX.search(flat_config[k])
263             if not match:
264                 keys_to_check.remove(k)
265                 continue
266 
267             # the matched_string includes "${}"; the matched_key is just the inner value
268             matched_string = match.group(0)
269             matched_key = match.group(1)
270 
271             # get the referenced key from the config value
272             ref_key = collections.CompoundKey(matched_key.split("."))
273             # get the value corresponding to the referenced key
274             ref_value = flat_config.get(ref_key, "")
275 
276             # if the matched was the entire value, replace it with the interpolated value
277             if flat_config[k] == matched_string:
278                 flat_config[k] = ref_value
279             # if it was a partial match, then drop the interpolated value into the string
280             else:
281                 flat_config[k] = flat_config[k].replace(
282                     matched_string, str(ref_value), 1
283                 )
284 
285     return cast(Config, collections.flatdict_to_dict(flat_config, dct_class=Config))
286 
287 
288 def load_configuration(
289     path: str,
290     user_config_path: str = None,
291     backend_config_path: str = None,
292     env_var_prefix: str = None,
293 ) -> Config:
294     """
295     Loads a configuration from a known location.
296 
297     Args:
298         - path (str): the path to the TOML configuration file
299         - user_config_path (str): an optional path to a user config file. If a user config
300             is provided, it will be used to update the main config prior to interpolation
301         - env_var_prefix (str): any env vars matching this prefix will be used to create
302             configuration values
303 
304     Returns:
305         - Config
306     """
307 
308     # load default config
309     default_config = load_toml(path)
310 
311     # load user config
312     if user_config_path and os.path.isfile(str(interpolate_env_vars(user_config_path))):
313         user_config = load_toml(user_config_path)
314         # merge user config into default config
315         default_config = cast(
316             dict, collections.merge_dicts(default_config, user_config)
317         )
318 
319     # load backend config
320     if backend_config_path and os.path.isfile(
321         str(interpolate_env_vars(backend_config_path))
322     ):
323         backend_config = load_toml(backend_config_path)
324         # merge backend config into default config
325         default_config = cast(
326             dict, collections.merge_dicts(default_config, backend_config)
327         )
328 
329     # interpolate after user config has already been merged
330     config = interpolate_config(default_config, env_var_prefix=env_var_prefix)
331 
332     validate_config(config)
333     return config
334 
335 
336 # load prefect configuration
337 config = load_configuration(
338     path=DEFAULT_CONFIG,
339     user_config_path=USER_CONFIG,
340     backend_config_path=BACKEND_CONFIG,
341     env_var_prefix=ENV_VAR_PREFIX,
342 )
343 
344 # add task defaults
345 config = process_task_defaults(config)
346
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/prefect/configuration.py b/src/prefect/configuration.py
--- a/src/prefect/configuration.py
+++ b/src/prefect/configuration.py
@@ -1,6 +1,7 @@
 import datetime
 import os
 import re
+from ast import literal_eval
 from typing import Optional, Union, cast
 
 import toml
@@ -41,14 +42,13 @@
     Maps:
         - "true" (any capitalization) to `True`
         - "false" (any capitalization) to `False`
-        - integers to `int`
-        - floats to `float`
-
+        - any other valid literal Python syntax interpretable by ast.literal_eval
+        
     Arguments:
         - val (str): the string value of an environment variable
 
     Returns:
-        Union[bool, int, float, str]: the type-cast env var value
+        Union[bool, int, float, str, dict, list, None, tuple]: the type-cast env var value
     """
 
     # bool
@@ -57,19 +57,10 @@
     elif val.upper() == "FALSE":
         return False
 
-    # int
-    try:
-        val_as_int = int(val)
-        if str(val_as_int) == val:
-            return val_as_int
-    except Exception:
-        pass
-
-    # float
+    # dicts, ints, floats, or any other literal Python syntax
     try:
-        val_as_float = float(val)
-        if str(val_as_float) == val:
-            return val_as_float
+        val_as_obj = literal_eval(val)
+        return val_as_obj
     except Exception:
         pass

{"golden_diff": "diff --git a/src/prefect/configuration.py b/src/prefect/configuration.py\n--- a/src/prefect/configuration.py\n+++ b/src/prefect/configuration.py\n@@ -1,6 +1,7 @@\n import datetime\n import os\n import re\n+from ast import literal_eval\n from typing import Optional, Union, cast\n \n import toml\n@@ -41,14 +42,13 @@\n     Maps:\n         - \"true\" (any capitalization) to `True`\n         - \"false\" (any capitalization) to `False`\n-        - integers to `int`\n-        - floats to `float`\n-\n+        - any other valid literal Python syntax interpretable by ast.literal_eval\n+        \n     Arguments:\n         - val (str): the string value of an environment variable\n \n     Returns:\n-        Union[bool, int, float, str]: the type-cast env var value\n+        Union[bool, int, float, str, dict, list, None, tuple]: the type-cast env var value\n     \"\"\"\n \n     # bool\n@@ -57,19 +57,10 @@\n     elif val.upper() == \"FALSE\":\n         return False\n \n-    # int\n-    try:\n-        val_as_int = int(val)\n-        if str(val_as_int) == val:\n-            return val_as_int\n-    except Exception:\n-        pass\n-\n-    # float\n+    # dicts, ints, floats, or any other literal Python syntax\n     try:\n-        val_as_float = float(val)\n-        if str(val_as_float) == val:\n-            return val_as_float\n+        val_as_obj = literal_eval(val)\n+        return val_as_obj\n     except Exception:\n         pass\n", "issue": "Use literal_eval for parsing configuration values\n## Current behavior\r\n*Please describe how the feature works today*\r\nCurrently, Prefect takes a [hardcoded stab](https://github.com/PrefectHQ/prefect/blob/master/src/prefect/configuration.py#L37) at converting strings to Python objects. We could replace this function with `ast.literal_eval()` and instantly gain a more flexible cast, including the ability to natively handle lists and dicts. \r\n\r\n\r\n\r\n## Proposed behavior\r\n*Please describe your proposed change to the current behavior*\r\n\r\nCurrently, setting `PREFECT__X__Y=\"['a', 'b', 3]\"` results in `prefect.config.x.y == \"['a', 'b', 3]\"`. This means some Prefect objects (thinking of environments, but also including the config itself) must re-parse config options. This is after we already make an attempt to coerce some values to known types. Replacing this with `ast.literal_eval()` would result in `prefect.config.x.y == ['a', 'b', 3]`.\r\n\r\n\r\n## Example\r\n*Please give an example of how the enhancement would be useful*\r\n\n", "before_files": [{"content": "import datetime\nimport os\nimport re\nfrom typing import Optional, Union, cast\n\nimport toml\nfrom box import Box\n\nfrom prefect.utilities import collections\n\nDEFAULT_CONFIG = os.path.join(os.path.dirname(__file__), \"config.toml\")\nUSER_CONFIG = os.getenv(\"PREFECT__USER_CONFIG_PATH\", \"~/.prefect/config.toml\")\nBACKEND_CONFIG = os.getenv(\"PREFECT__BACKEND_CONFIG_PATH\", \"~/.prefect/backend.toml\")\nENV_VAR_PREFIX = \"PREFECT\"\nINTERPOLATION_REGEX = re.compile(r\"\\${(.[^${}]*)}\")\n\n\nclass Config(Box):\n    \"\"\"\n    A config is a Box subclass\n    \"\"\"\n\n    def copy(self) -> \"Config\":\n        \"\"\"\n        Create a recursive copy of the config. Each level of the Config is a new Config object, so\n        modifying keys won't affect the original Config object. However, values are not\n        deep-copied, and mutations can affect the original.\n        \"\"\"\n        new_config = Config()\n        for key, value in self.items():\n            if isinstance(value, Config):\n                value = value.copy()\n            new_config[key] = value\n        return new_config\n\n\ndef string_to_type(val: str) -> Union[bool, int, float, str]:\n    \"\"\"\n    Helper function for transforming string env var values into typed values.\n\n    Maps:\n        - \"true\" (any capitalization) to `True`\n        - \"false\" (any capitalization) to `False`\n        - integers to `int`\n        - floats to `float`\n\n    Arguments:\n        - val (str): the string value of an environment variable\n\n    Returns:\n        Union[bool, int, float, str]: the type-cast env var value\n    \"\"\"\n\n    # bool\n    if val.upper() == \"TRUE\":\n        return True\n    elif val.upper() == \"FALSE\":\n        return False\n\n    # int\n    try:\n        val_as_int = int(val)\n        if str(val_as_int) == val:\n            return val_as_int\n    except Exception:\n        pass\n\n    # float\n    try:\n        val_as_float = float(val)\n        if str(val_as_float) == val:\n            return val_as_float\n    except Exception:\n        pass\n\n    # return string value\n    return val\n\n\ndef interpolate_env_vars(env_var: str) -> Optional[Union[bool, int, float, str]]:\n    \"\"\"\n    Expands (potentially nested) env vars by repeatedly applying\n    `expandvars` and `expanduser` until interpolation stops having\n    any effect.\n    \"\"\"\n    if not env_var or not isinstance(env_var, str):\n        return env_var\n\n    counter = 0\n\n    while counter < 10:\n        interpolated = os.path.expanduser(os.path.expandvars(str(env_var)))\n        if interpolated == env_var:\n            # if a change was made, apply string-to-type casts; otherwise leave alone\n            # this is because we don't want to override TOML type-casting if this function\n            # is applied to a non-interpolated value\n            if counter > 1:\n                interpolated = string_to_type(interpolated)  # type: ignore\n            return interpolated\n        else:\n            env_var = interpolated\n        counter += 1\n\n    return None\n\n\ndef create_user_config(dest_path: str, source_path: str = DEFAULT_CONFIG) -> None:\n    \"\"\"\n    Copies the default configuration to a user-customizable file at `dest_path`\n    \"\"\"\n    dest_path = cast(str, interpolate_env_vars(dest_path))\n    if os.path.isfile(dest_path):\n        raise ValueError(\"File already exists: {}\".format(dest_path))\n    os.makedirs(os.path.dirname(dest_path), exist_ok=True)\n\n    with open(dest_path, \"w\") as dest:\n        with open(source_path, \"r\") as source:\n            dest.write(source.read())\n\n\n# Process Config -------------------------------------------------------------\n\n\ndef process_task_defaults(config: Config) -> Config:\n    \"\"\"\n    Converts task defaults from basic types to Python objects like timedeltas\n\n    Args:\n        - config (Config): the configuration to modify\n    \"\"\"\n    # make sure defaults exists\n    defaults = config.setdefault(\"tasks\", {}).setdefault(\"defaults\", {})\n\n    # max_retries defaults to 0 if not set, False, or None\n    if not defaults.setdefault(\"max_retries\", 0):\n        defaults.max_retries = 0\n    defaults.max_retries = defaults.get(\"max_retries\", 0) or 0\n\n    # retry_delay defaults to None if not set - also check for False because TOML has no NULL\n    if defaults.setdefault(\"retry_delay\", False) is False:\n        defaults.retry_delay = None\n    elif isinstance(defaults.retry_delay, int):\n        defaults.retry_delay = datetime.timedelta(seconds=defaults.retry_delay)\n\n    # timeout defaults to None if not set - also check for False because TOML has no NULL\n    if defaults.setdefault(\"timeout\", False) is False:\n        defaults.timeout = None\n    elif isinstance(defaults.timeout, int):\n        defaults.timeout = datetime.timedelta(seconds=defaults.timeout)\n\n    return config\n\n\n# Validation ------------------------------------------------------------------\n\n\ndef validate_config(config: Config) -> None:\n    \"\"\"\n    Validates that the configuration file is valid.\n        - keys do not shadow Config methods\n\n    Note that this is performed when the config is first loaded, but not after.\n    \"\"\"\n\n    def check_valid_keys(config: Config) -> None:\n        \"\"\"\n        Recursively check that keys do not shadow methods of the Config object\n        \"\"\"\n        invalid_keys = dir(Config)\n        for k, v in config.items():\n            if k in invalid_keys:\n                raise ValueError('Invalid config key: \"{}\"'.format(k))\n            if isinstance(v, Config):\n                check_valid_keys(v)\n\n    check_valid_keys(config)\n\n\n# Load configuration ----------------------------------------------------------\n\n\ndef load_toml(path: str) -> dict:\n    \"\"\"\n    Loads a config dictionary from TOML\n    \"\"\"\n    return {\n        key: value\n        for key, value in toml.load(cast(str, interpolate_env_vars(path))).items()\n    }\n\n\ndef interpolate_config(config: dict, env_var_prefix: str = None) -> Config:\n    \"\"\"\n    Processes a config dictionary, such as the one loaded from `load_toml`.\n    \"\"\"\n\n    # toml supports nested dicts, so we work with a flattened representation to do any\n    # requested interpolation\n    flat_config = collections.dict_to_flatdict(config)\n\n    # --------------------- Interpolate env vars -----------------------\n    # check if any env var sets a configuration value with the format:\n    #     [ENV_VAR_PREFIX]__[Section]__[Optional Sub-Sections...]__[Key] = Value\n    # and if it does, add it to the config file.\n\n    if env_var_prefix:\n\n        for env_var, env_var_value in os.environ.items():\n            if env_var.startswith(env_var_prefix + \"__\"):\n\n                # strip the prefix off the env var\n                env_var_option = env_var[len(env_var_prefix + \"__\") :]\n\n                # make sure the resulting env var has at least one delimitied section and key\n                if \"__\" not in env_var:\n                    continue\n\n                # env vars with escaped characters are interpreted as literal \"\\\", which\n                # Python helpfully escapes with a second \"\\\". This step makes sure that\n                # escaped characters are properly interpreted.\n                value = cast(str, env_var_value.encode().decode(\"unicode_escape\"))\n\n                # place the env var in the flat config as a compound key\n                if env_var_option.upper().startswith(\"CONTEXT__SECRETS\"):\n                    formatted_option = env_var_option.split(\"__\")\n                    formatted_option[:-1] = [\n                        val.lower() for val in formatted_option[:-1]\n                    ]\n                    config_option = collections.CompoundKey(formatted_option)\n                else:\n                    config_option = collections.CompoundKey(\n                        env_var_option.lower().split(\"__\")\n                    )\n\n                flat_config[config_option] = string_to_type(\n                    cast(str, interpolate_env_vars(value))\n                )\n\n    # interpolate any env vars referenced\n    for k, v in list(flat_config.items()):\n        flat_config[k] = interpolate_env_vars(v)\n\n    # --------------------- Interpolate other config keys -----------------\n    # TOML doesn't support references to other keys... but we do!\n    # This has the potential to lead to nasty recursions, so we check at most 10 times.\n    # we use a set called \"keys_to_check\" to track only the ones of interest, so we aren't\n    # checking every key every time.\n\n    keys_to_check = set(flat_config.keys())\n\n    for _ in range(10):\n\n        # iterate over every key and value to check if the value uses interpolation\n        for k in list(keys_to_check):\n\n            # if the value isn't a string, it can't be a reference, so we exit\n            if not isinstance(flat_config[k], str):\n                keys_to_check.remove(k)\n                continue\n\n            # see if the ${...} syntax was used in the value and exit if it wasn't\n            match = INTERPOLATION_REGEX.search(flat_config[k])\n            if not match:\n                keys_to_check.remove(k)\n                continue\n\n            # the matched_string includes \"${}\"; the matched_key is just the inner value\n            matched_string = match.group(0)\n            matched_key = match.group(1)\n\n            # get the referenced key from the config value\n            ref_key = collections.CompoundKey(matched_key.split(\".\"))\n            # get the value corresponding to the referenced key\n            ref_value = flat_config.get(ref_key, \"\")\n\n            # if the matched was the entire value, replace it with the interpolated value\n            if flat_config[k] == matched_string:\n                flat_config[k] = ref_value\n            # if it was a partial match, then drop the interpolated value into the string\n            else:\n                flat_config[k] = flat_config[k].replace(\n                    matched_string, str(ref_value), 1\n                )\n\n    return cast(Config, collections.flatdict_to_dict(flat_config, dct_class=Config))\n\n\ndef load_configuration(\n    path: str,\n    user_config_path: str = None,\n    backend_config_path: str = None,\n    env_var_prefix: str = None,\n) -> Config:\n    \"\"\"\n    Loads a configuration from a known location.\n\n    Args:\n        - path (str): the path to the TOML configuration file\n        - user_config_path (str): an optional path to a user config file. If a user config\n            is provided, it will be used to update the main config prior to interpolation\n        - env_var_prefix (str): any env vars matching this prefix will be used to create\n            configuration values\n\n    Returns:\n        - Config\n    \"\"\"\n\n    # load default config\n    default_config = load_toml(path)\n\n    # load user config\n    if user_config_path and os.path.isfile(str(interpolate_env_vars(user_config_path))):\n        user_config = load_toml(user_config_path)\n        # merge user config into default config\n        default_config = cast(\n            dict, collections.merge_dicts(default_config, user_config)\n        )\n\n    # load backend config\n    if backend_config_path and os.path.isfile(\n        str(interpolate_env_vars(backend_config_path))\n    ):\n        backend_config = load_toml(backend_config_path)\n        # merge backend config into default config\n        default_config = cast(\n            dict, collections.merge_dicts(default_config, backend_config)\n        )\n\n    # interpolate after user config has already been merged\n    config = interpolate_config(default_config, env_var_prefix=env_var_prefix)\n\n    validate_config(config)\n    return config\n\n\n# load prefect configuration\nconfig = load_configuration(\n    path=DEFAULT_CONFIG,\n    user_config_path=USER_CONFIG,\n    backend_config_path=BACKEND_CONFIG,\n    env_var_prefix=ENV_VAR_PREFIX,\n)\n\n# add task defaults\nconfig = process_task_defaults(config)\n", "path": "src/prefect/configuration.py"}], "after_files": [{"content": "import datetime\nimport os\nimport re\nfrom ast import literal_eval\nfrom typing import Optional, Union, cast\n\nimport toml\nfrom box import Box\n\nfrom prefect.utilities import collections\n\nDEFAULT_CONFIG = os.path.join(os.path.dirname(__file__), \"config.toml\")\nUSER_CONFIG = os.getenv(\"PREFECT__USER_CONFIG_PATH\", \"~/.prefect/config.toml\")\nBACKEND_CONFIG = os.getenv(\"PREFECT__BACKEND_CONFIG_PATH\", \"~/.prefect/backend.toml\")\nENV_VAR_PREFIX = \"PREFECT\"\nINTERPOLATION_REGEX = re.compile(r\"\\${(.[^${}]*)}\")\n\n\nclass Config(Box):\n    \"\"\"\n    A config is a Box subclass\n    \"\"\"\n\n    def copy(self) -> \"Config\":\n        \"\"\"\n        Create a recursive copy of the config. Each level of the Config is a new Config object, so\n        modifying keys won't affect the original Config object. However, values are not\n        deep-copied, and mutations can affect the original.\n        \"\"\"\n        new_config = Config()\n        for key, value in self.items():\n            if isinstance(value, Config):\n                value = value.copy()\n            new_config[key] = value\n        return new_config\n\n\ndef string_to_type(val: str) -> Union[bool, int, float, str]:\n    \"\"\"\n    Helper function for transforming string env var values into typed values.\n\n    Maps:\n        - \"true\" (any capitalization) to `True`\n        - \"false\" (any capitalization) to `False`\n        - any other valid literal Python syntax interpretable by ast.literal_eval\n        \n    Arguments:\n        - val (str): the string value of an environment variable\n\n    Returns:\n        Union[bool, int, float, str, dict, list, None, tuple]: the type-cast env var value\n    \"\"\"\n\n    # bool\n    if val.upper() == \"TRUE\":\n        return True\n    elif val.upper() == \"FALSE\":\n        return False\n\n    # dicts, ints, floats, or any other literal Python syntax\n    try:\n        val_as_obj = literal_eval(val)\n        return val_as_obj\n    except Exception:\n        pass\n\n    # return string value\n    return val\n\n\ndef interpolate_env_vars(env_var: str) -> Optional[Union[bool, int, float, str]]:\n    \"\"\"\n    Expands (potentially nested) env vars by repeatedly applying\n    `expandvars` and `expanduser` until interpolation stops having\n    any effect.\n    \"\"\"\n    if not env_var or not isinstance(env_var, str):\n        return env_var\n\n    counter = 0\n\n    while counter < 10:\n        interpolated = os.path.expanduser(os.path.expandvars(str(env_var)))\n        if interpolated == env_var:\n            # if a change was made, apply string-to-type casts; otherwise leave alone\n            # this is because we don't want to override TOML type-casting if this function\n            # is applied to a non-interpolated value\n            if counter > 1:\n                interpolated = string_to_type(interpolated)  # type: ignore\n            return interpolated\n        else:\n            env_var = interpolated\n        counter += 1\n\n    return None\n\n\ndef create_user_config(dest_path: str, source_path: str = DEFAULT_CONFIG) -> None:\n    \"\"\"\n    Copies the default configuration to a user-customizable file at `dest_path`\n    \"\"\"\n    dest_path = cast(str, interpolate_env_vars(dest_path))\n    if os.path.isfile(dest_path):\n        raise ValueError(\"File already exists: {}\".format(dest_path))\n    os.makedirs(os.path.dirname(dest_path), exist_ok=True)\n\n    with open(dest_path, \"w\") as dest:\n        with open(source_path, \"r\") as source:\n            dest.write(source.read())\n\n\n# Process Config -------------------------------------------------------------\n\n\ndef process_task_defaults(config: Config) -> Config:\n    \"\"\"\n    Converts task defaults from basic types to Python objects like timedeltas\n\n    Args:\n        - config (Config): the configuration to modify\n    \"\"\"\n    # make sure defaults exists\n    defaults = config.setdefault(\"tasks\", {}).setdefault(\"defaults\", {})\n\n    # max_retries defaults to 0 if not set, False, or None\n    if not defaults.setdefault(\"max_retries\", 0):\n        defaults.max_retries = 0\n    defaults.max_retries = defaults.get(\"max_retries\", 0) or 0\n\n    # retry_delay defaults to None if not set - also check for False because TOML has no NULL\n    if defaults.setdefault(\"retry_delay\", False) is False:\n        defaults.retry_delay = None\n    elif isinstance(defaults.retry_delay, int):\n        defaults.retry_delay = datetime.timedelta(seconds=defaults.retry_delay)\n\n    # timeout defaults to None if not set - also check for False because TOML has no NULL\n    if defaults.setdefault(\"timeout\", False) is False:\n        defaults.timeout = None\n    elif isinstance(defaults.timeout, int):\n        defaults.timeout = datetime.timedelta(seconds=defaults.timeout)\n\n    return config\n\n\n# Validation ------------------------------------------------------------------\n\n\ndef validate_config(config: Config) -> None:\n    \"\"\"\n    Validates that the configuration file is valid.\n        - keys do not shadow Config methods\n\n    Note that this is performed when the config is first loaded, but not after.\n    \"\"\"\n\n    def check_valid_keys(config: Config) -> None:\n        \"\"\"\n        Recursively check that keys do not shadow methods of the Config object\n        \"\"\"\n        invalid_keys = dir(Config)\n        for k, v in config.items():\n            if k in invalid_keys:\n                raise ValueError('Invalid config key: \"{}\"'.format(k))\n            if isinstance(v, Config):\n                check_valid_keys(v)\n\n    check_valid_keys(config)\n\n\n# Load configuration ----------------------------------------------------------\n\n\ndef load_toml(path: str) -> dict:\n    \"\"\"\n    Loads a config dictionary from TOML\n    \"\"\"\n    return {\n        key: value\n        for key, value in toml.load(cast(str, interpolate_env_vars(path))).items()\n    }\n\n\ndef interpolate_config(config: dict, env_var_prefix: str = None) -> Config:\n    \"\"\"\n    Processes a config dictionary, such as the one loaded from `load_toml`.\n    \"\"\"\n\n    # toml supports nested dicts, so we work with a flattened representation to do any\n    # requested interpolation\n    flat_config = collections.dict_to_flatdict(config)\n\n    # --------------------- Interpolate env vars -----------------------\n    # check if any env var sets a configuration value with the format:\n    #     [ENV_VAR_PREFIX]__[Section]__[Optional Sub-Sections...]__[Key] = Value\n    # and if it does, add it to the config file.\n\n    if env_var_prefix:\n\n        for env_var, env_var_value in os.environ.items():\n            if env_var.startswith(env_var_prefix + \"__\"):\n\n                # strip the prefix off the env var\n                env_var_option = env_var[len(env_var_prefix + \"__\") :]\n\n                # make sure the resulting env var has at least one delimitied section and key\n                if \"__\" not in env_var:\n                    continue\n\n                # env vars with escaped characters are interpreted as literal \"\\\", which\n                # Python helpfully escapes with a second \"\\\". This step makes sure that\n                # escaped characters are properly interpreted.\n                value = cast(str, env_var_value.encode().decode(\"unicode_escape\"))\n\n                # place the env var in the flat config as a compound key\n                if env_var_option.upper().startswith(\"CONTEXT__SECRETS\"):\n                    formatted_option = env_var_option.split(\"__\")\n                    formatted_option[:-1] = [\n                        val.lower() for val in formatted_option[:-1]\n                    ]\n                    config_option = collections.CompoundKey(formatted_option)\n                else:\n                    config_option = collections.CompoundKey(\n                        env_var_option.lower().split(\"__\")\n                    )\n\n                flat_config[config_option] = string_to_type(\n                    cast(str, interpolate_env_vars(value))\n                )\n\n    # interpolate any env vars referenced\n    for k, v in list(flat_config.items()):\n        flat_config[k] = interpolate_env_vars(v)\n\n    # --------------------- Interpolate other config keys -----------------\n    # TOML doesn't support references to other keys... but we do!\n    # This has the potential to lead to nasty recursions, so we check at most 10 times.\n    # we use a set called \"keys_to_check\" to track only the ones of interest, so we aren't\n    # checking every key every time.\n\n    keys_to_check = set(flat_config.keys())\n\n    for _ in range(10):\n\n        # iterate over every key and value to check if the value uses interpolation\n        for k in list(keys_to_check):\n\n            # if the value isn't a string, it can't be a reference, so we exit\n            if not isinstance(flat_config[k], str):\n                keys_to_check.remove(k)\n                continue\n\n            # see if the ${...} syntax was used in the value and exit if it wasn't\n            match = INTERPOLATION_REGEX.search(flat_config[k])\n            if not match:\n                keys_to_check.remove(k)\n                continue\n\n            # the matched_string includes \"${}\"; the matched_key is just the inner value\n            matched_string = match.group(0)\n            matched_key = match.group(1)\n\n            # get the referenced key from the config value\n            ref_key = collections.CompoundKey(matched_key.split(\".\"))\n            # get the value corresponding to the referenced key\n            ref_value = flat_config.get(ref_key, \"\")\n\n            # if the matched was the entire value, replace it with the interpolated value\n            if flat_config[k] == matched_string:\n                flat_config[k] = ref_value\n            # if it was a partial match, then drop the interpolated value into the string\n            else:\n                flat_config[k] = flat_config[k].replace(\n                    matched_string, str(ref_value), 1\n                )\n\n    return cast(Config, collections.flatdict_to_dict(flat_config, dct_class=Config))\n\n\ndef load_configuration(\n    path: str,\n    user_config_path: str = None,\n    backend_config_path: str = None,\n    env_var_prefix: str = None,\n) -> Config:\n    \"\"\"\n    Loads a configuration from a known location.\n\n    Args:\n        - path (str): the path to the TOML configuration file\n        - user_config_path (str): an optional path to a user config file. If a user config\n            is provided, it will be used to update the main config prior to interpolation\n        - env_var_prefix (str): any env vars matching this prefix will be used to create\n            configuration values\n\n    Returns:\n        - Config\n    \"\"\"\n\n    # load default config\n    default_config = load_toml(path)\n\n    # load user config\n    if user_config_path and os.path.isfile(str(interpolate_env_vars(user_config_path))):\n        user_config = load_toml(user_config_path)\n        # merge user config into default config\n        default_config = cast(\n            dict, collections.merge_dicts(default_config, user_config)\n        )\n\n    # load backend config\n    if backend_config_path and os.path.isfile(\n        str(interpolate_env_vars(backend_config_path))\n    ):\n        backend_config = load_toml(backend_config_path)\n        # merge backend config into default config\n        default_config = cast(\n            dict, collections.merge_dicts(default_config, backend_config)\n        )\n\n    # interpolate after user config has already been merged\n    config = interpolate_config(default_config, env_var_prefix=env_var_prefix)\n\n    validate_config(config)\n    return config\n\n\n# load prefect configuration\nconfig = load_configuration(\n    path=DEFAULT_CONFIG,\n    user_config_path=USER_CONFIG,\n    backend_config_path=BACKEND_CONFIG,\n    env_var_prefix=ENV_VAR_PREFIX,\n)\n\n# add task defaults\nconfig = process_task_defaults(config)\n", "path": "src/prefect/configuration.py"}]}