Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions okta/config/config_setter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import os

import yaml
from flatdict import FlatDict

from okta.constants import _GLOBAL_YAML_PATH, _LOCAL_YAML_PATH
from okta.utils import flatten_dict, unflatten_dict, remove_empty_values


class ConfigSetter:
Expand Down Expand Up @@ -68,14 +68,7 @@ def _prune_config(self, config):
This method cleans up the configuration object by removing fields
with no value
"""
# Flatten dictionary to account for nested dictionary
flat_current_config = FlatDict(config, delimiter="_")
# Iterate through keys and remove if value is still empty string
for key in flat_current_config.keys():
if flat_current_config.get(key) == "":
del flat_current_config[key]

return flat_current_config.as_dict()
return remove_empty_values(config)

def _update_config(self):
"""
Expand Down Expand Up @@ -124,17 +117,20 @@ def _apply_config(self, new_config: dict):
"""
# Update current configuration with new configuration
# Flatten both dictionaries to account for nested dictionary values
flat_current_client = FlatDict(self._config["client"], delimiter="_")
flat_current_testing = FlatDict(self._config["testing"], delimiter="_")
flat_current_client = flatten_dict(self._config["client"], delimiter="::")
flat_current_testing = flatten_dict(self._config["testing"], delimiter="::")

flat_new_client = flatten_dict(new_config.get("client", {}), delimiter="::")
flat_new_testing = flatten_dict(new_config.get("testing", {}), delimiter="::")

flat_new_client = FlatDict(new_config.get("client", {}), delimiter="_")
flat_new_testing = FlatDict(new_config.get("testing", {}), delimiter="_")
# Update with new values
flat_current_client.update(flat_new_client)
flat_current_testing.update(flat_new_testing)

# Update values in current config and unflatten
self._config = {
"client": flat_current_client.as_dict(),
"testing": flat_current_testing.as_dict(),
"client": unflatten_dict(flat_current_client, delimiter="::"),
"testing": unflatten_dict(flat_current_testing, delimiter="::"),
}

def _apply_yaml_config(self, path: str):
Expand All @@ -148,6 +144,9 @@ def _apply_yaml_config(self, path: str):
with open(path, "r") as file:
# Open file stream and attempt to load YAML
config = yaml.load(file, Loader=yaml.SafeLoader)
# Handle empty YAML files or files with only comments
if config is None:
config = {}
# Apply acquired config to configuration
self._apply_config(config.get("okta", {}))

Expand All @@ -156,18 +155,19 @@ def _apply_env_config(self):
This method checks the environment variables for any OKTA
configuration parameters and applies them if available.
"""
# Flatten current config and join with underscores
# Flatten current config with :: delimiter for internal processing
# (for environment variable format)
flattened_config = FlatDict(self._config, delimiter="_")
flattened_config = flatten_dict(self._config, delimiter="::")
flattened_keys = flattened_config.keys()

# Create empty result config and populate
updated_config = FlatDict({}, delimiter="_")
updated_config = {}

# Go through keys and search for it in the environment vars
# using the format described in the README
for key in flattened_keys:
env_key = ConfigSetter._OKTA + "_" + key.upper()
# Convert internal :: delimiter to _ for environment variable name
env_key = ConfigSetter._OKTA + "_" + key.replace("::", "_").upper()
env_value = os.environ.get(env_key, None)

if env_value is not None:
Expand All @@ -176,5 +176,6 @@ def _apply_env_config(self):
updated_config[key] = env_value.split(",")
else:
updated_config[key] = env_value
# apply to current configuration
self._apply_config(updated_config.as_dict())

# Apply to current configuration
self._apply_config(unflatten_dict(updated_config, delimiter="::"))
125 changes: 125 additions & 0 deletions okta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from datetime import datetime as dt
from enum import Enum
from typing import Any, Dict
from urllib.parse import urlsplit, urlunsplit

from okta.constants import DATETIME_FORMAT, EPOCH_DAY, EPOCH_MONTH, EPOCH_YEAR
Expand Down Expand Up @@ -78,3 +79,127 @@ def convert_absolute_url_into_relative_url(absolute_url):
"""
url_parts = urlsplit(absolute_url)
return urlunsplit(("", "", url_parts[2], url_parts[3], url_parts[4]))


# Dictionary utility functions for nested dictionary operations
# These replace the external flatdict dependency


def flatten_dict(d: Dict[str, Any], parent_key: str = '', delimiter: str = '::') -> Dict[str, Any]:
"""
Flatten a nested dictionary into a single-level dictionary.

Args:
d: The dictionary to flatten
parent_key: The base key to prepend to all keys (used in recursion)
delimiter: The delimiter to use when joining keys (default '::' to avoid collision with snake_case)

Returns:
A flattened dictionary with delimited keys

Examples:
>>> flatten_dict({'a': {'b': 1, 'c': 2}})
{'a::b': 1, 'a::c': 2}

>>> flatten_dict({'x': {'y': {'z': 3}}})
{'x::y::z': 3}

>>> flatten_dict({'user_name': {'first_name': 'John'}})
{'user_name::first_name': 'John'}
"""
items = []
for key, value in d.items():
new_key = f"{parent_key}{delimiter}{key}" if parent_key else key
if isinstance(value, dict):
items.extend(flatten_dict(value, new_key, delimiter).items())
else:
items.append((new_key, value))
return dict(items)


def unflatten_dict(d: Dict[str, Any], delimiter: str = '::') -> Dict[str, Any]:
"""
Unflatten a dictionary with delimited keys into a nested structure.

Args:
d: The flattened dictionary
delimiter: The delimiter used in the keys

Returns:
A nested dictionary

Examples:
>>> unflatten_dict({'a_b': 1, 'a_c': 2})
{'a': {'b': 1, 'c': 2}}

>>> unflatten_dict({'x_y_z': 3})
{'x': {'y': {'z': 3}}}
"""
result: Dict[str, Any] = {}
for key, value in d.items():
parts = key.split(delimiter)
current = result
for part in parts[:-1]:
if part not in current:
current[part] = {}
current = current[part]
current[parts[-1]] = value
return result


def deep_merge(base: Dict[str, Any], updates: Dict[str, Any]) -> Dict[str, Any]:
"""
Deep merge two dictionaries, with updates overriding base values.

Args:
base: The base dictionary
updates: Dictionary with values to merge/override

Returns:
A new dictionary with merged values

Examples:
>>> deep_merge({'a': 1, 'b': 2}, {'b': 3, 'c': 4})
{'a': 1, 'b': 3, 'c': 4}

>>> deep_merge({'a': {'b': 1}}, {'a': {'c': 2}})
{'a': {'b': 1, 'c': 2}}
"""
result = base.copy()
for key, value in updates.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result


def remove_empty_values(d: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively remove empty string values from a nested dictionary.

Args:
d: The dictionary to clean

Returns:
A new dictionary with empty strings removed

Examples:
>>> remove_empty_values({'a': '', 'b': 'value'})
{'b': 'value'}

>>> remove_empty_values({'a': {'b': '', 'c': 1}})
{'a': {'c': 1}}

Note:
Only removes empty strings (""), not other falsy values like None, 0, False, []
"""
result = {}
for key, value in d.items():
if isinstance(value, dict):
nested = remove_empty_values(value)
if nested: # Only add if nested dict is not empty after cleaning
result[key] = nested
elif value != "":
result[key] = value
return result
3 changes: 1 addition & 2 deletions openapi/templates/requirements.mustache
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
aenum==3.1.11
aiohttp==3.12.14
blinker==1.9.0
flatdict==4.0.1
jwcrypto==1.5.6
pycryptodomex==3.23.0
pydantic==2.11.3
Expand All @@ -20,4 +19,4 @@ pytest-asyncio==0.26.0
pytest-mock==3.14.0
pytest-recording==0.13.2
tox==4.24.2
twine==6.1.0
twine==6.1.0
1 change: 0 additions & 1 deletion openapi/templates/setup.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ REQUIRES = [
"aenum >= 3.1.11",
"aiohttp >= 3.12.14",
"blinker >= 1.9.0",
"flatdict >= 4.0.1",
'jwcrypto >= 1.5.6',
"pycryptodomex >= 3.23.0",
"pydantic >= 2.11.3",
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
aenum==3.1.11
aiohttp==3.12.14
blinker==1.9.0
flatdict==4.0.1
jwcrypto==1.5.6
pycryptodomex==3.23.0
pydantic==2.11.3
Expand All @@ -20,4 +19,4 @@ pytest-asyncio==0.26.0
pytest-mock==3.14.0
pytest-recording==0.13.2
tox==4.24.2
twine==6.1.0
twine==6.1.0
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
"aenum >= 3.1.11",
"aiohttp >= 3.12.14",
"blinker >= 1.9.0",
"flatdict >= 4.0.1",
'jwcrypto >= 1.5.6',
"pycryptodomex >= 3.23.0",
"pydantic >= 2.11.3",
Expand Down
Loading