Skip to content

Commit 88da6c3

Browse files
authored
Feature/strip prefixes (#102)
* Add a new paramter to strip the prefixes * Strip prefixes, and fixed TOML tests
1 parent d36b16b commit 88da6c3

File tree

10 files changed

+178
-25
lines changed

10 files changed

+178
-25
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
8+
## [0.12.0] - 2024-07-23
9+
10+
### Added
11+
12+
- Granular `strip_prefix` parameters across different config types
13+
14+
### Fixed
15+
16+
- Unit tests for .toml files
17+
18+
### Changed
19+
20+
- Enviroment files are now loaded from filenames with a suffix of `.env` or starting with `.env`
21+
22+
723
## [0.11.0] - 2024-04-23
824

925
### Changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Source = "https://github.com/tr11/python-configuration"
2828
[project.optional-dependencies]
2929
# cloud
3030
aws = ["boto3>=1.28.20"]
31-
azure = ["azure-keyvault>=5.0.0"]
31+
azure = ["azure-keyvault>=4.2.0", "azure-identity"]
3232
gcp = ["google-cloud-secret-manager>=2.16.3"]
3333
vault = ["hvac>=1.1.1"]
3434
# file formats
@@ -148,6 +148,7 @@ module = [
148148
'hvac.exceptions',
149149
'jsonschema',
150150
'jsonschema.exceptions',
151+
'azure.identity',
151152
]
152153
ignore_missing_imports = true
153154

src/config/__init__.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
def config(
3333
*configs: Iterable,
3434
prefix: str = "",
35+
strip_prefix: bool = True,
3536
separator: Optional[str] = None,
3637
remove_level: int = 1,
3738
lowercase_keys: bool = False,
@@ -44,6 +45,7 @@ def config(
4445
Params:
4546
configs: iterable of configurations
4647
prefix: prefix to filter environment variables with
48+
strip_prefix: whether to strip the prefix
4749
remove_level: how many levels to remove from the resulting config
4850
lowercase_keys: whether to convert every key to lower case.
4951
ignore_missing_paths: whether to ignore failures from missing files/folders.
@@ -71,6 +73,9 @@ def config(
7173
if isinstance(config_, Mapping):
7274
instances.append(config_from_dict(config_, **default_kwargs))
7375
continue
76+
elif isinstance(config_, Configuration):
77+
instances.append(config_)
78+
continue
7479
elif isinstance(config_, str):
7580
if config_.endswith(".py"):
7681
config_ = ("python", config_, *default_args)
@@ -82,8 +87,8 @@ def config(
8287
config_ = ("toml", config_, True)
8388
elif config_.endswith(".ini"):
8489
config_ = ("ini", config_, True)
85-
elif config_.endswith(".env"):
86-
config_ = ("dotenv", config_, True)
90+
elif config_.endswith(".env") or config_.startswith(".env"):
91+
config_ = ("dotenv", config_, True, *default_args)
8792
elif os.path.isdir(config_):
8893
config_ = ("path", config_, remove_level)
8994
elif config_ in ("env", "environment"):
@@ -103,7 +108,13 @@ def config(
103108
instances.append(config_from_dict(*config_[1:], **default_kwargs))
104109
elif type_ in ("env", "environment"):
105110
params = list(config_[1:]) + default_args[(len(config_) - 1) :]
106-
instances.append(config_from_env(*params, **default_kwargs))
111+
instances.append(
112+
config_from_env(
113+
*params,
114+
**default_kwargs,
115+
strip_prefix=strip_prefix,
116+
),
117+
)
107118
elif type_ == "python":
108119
if len(config_) < 2:
109120
raise ValueError("No path specified for python module")
@@ -113,6 +124,7 @@ def config(
113124
*params,
114125
**default_kwargs,
115126
ignore_missing_paths=ignore_missing_paths,
127+
strip_prefix=strip_prefix,
116128
),
117129
)
118130
elif type_ == "json":
@@ -137,6 +149,7 @@ def config(
137149
*config_[1:],
138150
**default_kwargs,
139151
ignore_missing_paths=ignore_missing_paths,
152+
strip_prefix=strip_prefix,
140153
),
141154
)
142155
elif type_ == "ini":
@@ -145,6 +158,7 @@ def config(
145158
*config_[1:],
146159
**default_kwargs,
147160
ignore_missing_paths=ignore_missing_paths,
161+
strip_prefix=strip_prefix,
148162
),
149163
)
150164
elif type_ == "dotenv":
@@ -153,6 +167,7 @@ def config(
153167
*config_[1:],
154168
**default_kwargs,
155169
ignore_missing_paths=ignore_missing_paths,
170+
strip_prefix=strip_prefix,
156171
),
157172
)
158173
elif type_ == "path":
@@ -178,9 +193,10 @@ class EnvConfiguration(Configuration):
178193

179194
def __init__(
180195
self,
181-
prefix: str,
196+
prefix: str = "",
182197
separator: str = "__",
183198
*,
199+
strip_prefix: bool = True,
184200
lowercase_keys: bool = False,
185201
interpolate: InterpolateType = False,
186202
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -189,9 +205,11 @@ def __init__(
189205
190206
prefix: prefix to filter environment variables with
191207
separator: separator to replace by dots
208+
strip_prefix: whether to include the prefix
192209
lowercase_keys: whether to convert every key to lower case.
193210
"""
194211
self._prefix = prefix
212+
self._strip_prefix = strip_prefix
195213
self._separator = separator
196214
super().__init__(
197215
{},
@@ -207,9 +225,12 @@ def reload(self) -> None:
207225
for key, value in os.environ.items():
208226
if not key.startswith(self._prefix + self._separator):
209227
continue
210-
result[
211-
key[len(self._prefix) :].replace(self._separator, ".").strip(".")
212-
] = value
228+
if self._strip_prefix:
229+
result[
230+
key[len(self._prefix) :].replace(self._separator, ".").strip(".")
231+
] = value
232+
else:
233+
result[key.replace(self._separator, ".").strip(".")] = value
213234
super().__init__(
214235
result,
215236
lowercase_keys=self._lowercase,
@@ -222,6 +243,7 @@ def config_from_env(
222243
prefix: str,
223244
separator: str = "__",
224245
*,
246+
strip_prefix: bool = True,
225247
lowercase_keys: bool = False,
226248
interpolate: InterpolateType = False,
227249
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -231,6 +253,7 @@ def config_from_env(
231253
Params:
232254
prefix: prefix to filter environment variables with.
233255
separator: separator to replace by dots.
256+
strip_prefix: whether to include the prefix
234257
lowercase_keys: whether to convert every key to lower case.
235258
interpolate: whether to apply string interpolation when looking for items.
236259
@@ -240,6 +263,7 @@ def config_from_env(
240263
return EnvConfiguration(
241264
prefix,
242265
separator,
266+
strip_prefix=strip_prefix,
243267
lowercase_keys=lowercase_keys,
244268
interpolate=interpolate,
245269
interpolate_type=interpolate_type,
@@ -463,13 +487,15 @@ def __init__(
463487
read_from_file: bool = False,
464488
*,
465489
section_prefix: str = "",
490+
strip_prefix: bool = True,
466491
lowercase_keys: bool = False,
467492
interpolate: InterpolateType = False,
468493
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
469494
ignore_missing_paths: bool = False,
470495
):
471496
"""Class Constructor."""
472497
self._section_prefix = section_prefix
498+
self._strip_prefix = strip_prefix
473499
super().__init__(
474500
data=data,
475501
read_from_file=read_from_file,
@@ -502,8 +528,9 @@ def optionxform(self, optionstr: str) -> str:
502528
data = cast(str, data)
503529
cfg = ConfigParser()
504530
cfg.read_string(data)
531+
n = len(self._section_prefix) if self._strip_prefix else 0
505532
result = {
506-
section[len(self._section_prefix) :] + "." + k: v
533+
section[n:] + "." + k: v
507534
for section, values in cfg.items()
508535
for k, v in values.items()
509536
if section.startswith(self._section_prefix)
@@ -516,6 +543,7 @@ def config_from_ini(
516543
read_from_file: bool = False,
517544
*,
518545
section_prefix: str = "",
546+
strip_prefix: bool = True,
519547
lowercase_keys: bool = False,
520548
interpolate: InterpolateType = False,
521549
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -538,6 +566,7 @@ def config_from_ini(
538566
data,
539567
read_from_file,
540568
section_prefix=section_prefix,
569+
strip_prefix=strip_prefix,
541570
lowercase_keys=lowercase_keys,
542571
interpolate=interpolate,
543572
interpolate_type=interpolate_type,
@@ -555,14 +584,17 @@ def __init__(
555584
prefix: str = "",
556585
separator: str = "__",
557586
*,
587+
strip_prefix: bool = True,
558588
lowercase_keys: bool = False,
559589
interpolate: InterpolateType = False,
560590
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
561591
ignore_missing_paths: bool = False,
562592
):
563593
"""Class Constructor."""
564594
self._prefix = prefix
595+
self._strip_prefix = strip_prefix
565596
self._separator = separator
597+
566598
super().__init__(
567599
data=data,
568600
read_from_file=read_from_file,
@@ -589,8 +621,9 @@ def _reload(
589621
parse_env_line(x) for x in data.splitlines() if x and not x.startswith("#")
590622
)
591623

624+
n = len(self._prefix) if self._strip_prefix else 0
592625
result = {
593-
k[len(self._prefix) :].replace(self._separator, ".").strip("."): v
626+
k[n:].replace(self._separator, ".").strip("."): v
594627
for k, v in result.items()
595628
if k.startswith(self._prefix)
596629
}
@@ -604,6 +637,7 @@ def config_from_dotenv(
604637
prefix: str = "",
605638
separator: str = "__",
606639
*,
640+
strip_prefix: bool = True,
607641
lowercase_keys: bool = False,
608642
interpolate: InterpolateType = False,
609643
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -629,6 +663,7 @@ def config_from_dotenv(
629663
read_from_file,
630664
prefix=prefix,
631665
separator=separator,
666+
strip_prefix=strip_prefix,
632667
lowercase_keys=lowercase_keys,
633668
interpolate=interpolate,
634669
interpolate_type=interpolate_type,
@@ -645,6 +680,7 @@ def __init__(
645680
prefix: str = "",
646681
separator: str = "_",
647682
*,
683+
strip_prefix: bool = True,
648684
lowercase_keys: bool = False,
649685
interpolate: InterpolateType = False,
650686
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -677,6 +713,7 @@ def __init__(
677713
module = importlib.import_module(module)
678714
self._module: Optional[ModuleType] = module
679715
self._prefix = prefix
716+
self._strip_prefix = strip_prefix
680717
self._separator = separator
681718
except (FileNotFoundError, ModuleNotFoundError):
682719
if not ignore_missing_paths:
@@ -699,10 +736,9 @@ def reload(self) -> None:
699736
for x in dir(self._module)
700737
if not x.startswith("__") and x.startswith(self._prefix)
701738
]
739+
n = len(self._prefix) if self._strip_prefix else 0
702740
result = {
703-
k[len(self._prefix) :]
704-
.replace(self._separator, ".")
705-
.strip("."): getattr(self._module, k)
741+
k[n:].replace(self._separator, ".").strip("."): getattr(self._module, k)
706742
for k in variables
707743
}
708744
else:
@@ -720,6 +756,7 @@ def config_from_python(
720756
prefix: str = "",
721757
separator: str = "_",
722758
*,
759+
strip_prefix: bool = True,
723760
lowercase_keys: bool = False,
724761
interpolate: InterpolateType = False,
725762
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -741,6 +778,7 @@ def config_from_python(
741778
module,
742779
prefix,
743780
separator,
781+
strip_prefix=strip_prefix,
744782
lowercase_keys=lowercase_keys,
745783
interpolate=interpolate,
746784
interpolate_type=interpolate_type,
@@ -882,6 +920,7 @@ def __init__(
882920
read_from_file: bool = False,
883921
*,
884922
section_prefix: str = "",
923+
strip_prefix: bool = True,
885924
lowercase_keys: bool = False,
886925
interpolate: InterpolateType = False,
887926
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -894,6 +933,7 @@ def __init__(
894933
)
895934

896935
self._section_prefix = section_prefix
936+
self._strip_prefix = strip_prefix
897937
super().__init__(
898938
data=data,
899939
read_from_file=read_from_file,
@@ -920,8 +960,9 @@ def _reload(
920960
loaded = toml.loads(data)
921961
loaded = cast(dict, loaded)
922962

963+
n = len(self._section_prefix) if self._section_prefix else 0
923964
result = {
924-
k[len(self._section_prefix) :]: v
965+
k[n:]: v
925966
for k, v in self._flatten_dict(loaded).items()
926967
if k.startswith(self._section_prefix)
927968
}
@@ -934,6 +975,7 @@ def config_from_toml(
934975
read_from_file: bool = False,
935976
*,
936977
section_prefix: str = "",
978+
strip_prefix: bool = True,
937979
lowercase_keys: bool = False,
938980
interpolate: InterpolateType = False,
939981
interpolate_type: InterpolateEnumType = InterpolateEnumType.STANDARD,
@@ -955,6 +997,7 @@ def config_from_toml(
955997
data,
956998
read_from_file,
957999
section_prefix=section_prefix,
1000+
strip_prefix=strip_prefix,
9581001
lowercase_keys=lowercase_keys,
9591002
interpolate=interpolate,
9601003
interpolate_type=interpolate_type,

src/config/contrib/gcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class GCPSecretManagerConfiguration(Configuration):
3535
def __init__(
3636
self,
3737
project_id: str,
38-
credentials: Credentials = None,
38+
credentials: Optional[Credentials] = None,
3939
client_options: Optional[ClientOptions] = None,
4040
cache_expiration: int = 5 * 60,
4141
interpolate: InterpolateType = False,

tests/contrib/test_azure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
from config import config_from_dict
66

77
try:
8-
import azure
98
from config.contrib.azure import AzureKeyVaultConfiguration
109
from azure.core.exceptions import ResourceNotFoundError
10+
azure = True
1111
except ImportError: # pragma: no cover
1212
azure = None # type: ignore
13-
13+
raise
1414

1515
DICT = {
1616
"foo": "foo_val",

0 commit comments

Comments
 (0)