2024-01-04 19:30:54 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import typing
|
|
|
|
|
|
|
|
from keep_it_secret.fields import Field
|
|
|
|
|
|
|
|
TFields: typing.TypeAlias = dict[str, Field]
|
|
|
|
|
|
|
|
|
|
|
|
def process_class_attributes(attributes) -> tuple[dict[str, typing.Any], TFields]:
|
|
|
|
new_attributes: dict[str, typing.Any] = {}
|
|
|
|
fields: TFields = {}
|
|
|
|
|
|
|
|
for attribute_name, attribute in attributes.items():
|
|
|
|
if isinstance(attribute, Field) is True:
|
|
|
|
fields[attribute_name] = attribute
|
|
|
|
else:
|
|
|
|
new_attributes[attribute_name] = attribute
|
|
|
|
|
|
|
|
return new_attributes, fields
|
|
|
|
|
|
|
|
|
|
|
|
def field_property_factory(field_name: str, field: Field) -> property:
|
|
|
|
def getter(instance: Secrets) -> typing.Any:
|
|
|
|
if field_name not in instance.__secrets_data__:
|
|
|
|
field: Field = instance.__secrets_fields__[field_name]
|
|
|
|
instance.__secrets_data__[field_name] = field(instance)
|
|
|
|
|
|
|
|
return instance.__secrets_data__[field_name]
|
|
|
|
|
|
|
|
return property(fget=getter, doc=field.description)
|
|
|
|
|
|
|
|
|
|
|
|
class SecretsBase(type):
|
|
|
|
def __new__(cls, name, bases, attributes, **kwargs):
|
|
|
|
super_new = super().__new__
|
|
|
|
|
|
|
|
fields = {}
|
|
|
|
for base in bases:
|
|
|
|
if isinstance(base, SecretsBase) is True:
|
|
|
|
fields.update(base.__secrets_fields__)
|
|
|
|
|
|
|
|
new_attributes, cls_fields = process_class_attributes(attributes)
|
|
|
|
|
|
|
|
final_fields = {**fields, **cls_fields}
|
|
|
|
new_attributes['__secrets_fields__'] = final_fields
|
|
|
|
|
|
|
|
new_class = super_new(cls, name, bases, new_attributes, **kwargs)
|
|
|
|
|
|
|
|
for field_name, field in final_fields.items():
|
|
|
|
field.name = f'{name}.{field_name}'
|
|
|
|
|
|
|
|
setattr(
|
|
|
|
new_class,
|
|
|
|
field_name,
|
|
|
|
field_property_factory(field_name, field),
|
|
|
|
)
|
|
|
|
|
|
|
|
return new_class
|
|
|
|
|
|
|
|
|
|
|
|
class Secrets(metaclass=SecretsBase):
|
|
|
|
"""
|
|
|
|
The base Secrets class, used to declare application-specfic secrets
|
|
|
|
containers.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
class AppSecrets(Secrets):
|
|
|
|
secret_key: str = AbstractField.new()
|
|
|
|
db_password: str = EnvField.new('APP_DB_PASSWORD', required=True)
|
|
|
|
|
|
|
|
not_a_secret = 'spam'
|
|
|
|
|
|
|
|
def do_something(self) -> bool:
|
|
|
|
return 'eggs'
|
|
|
|
|
|
|
|
When instantiated, ``AppSecrets`` will evaluate each of the fields and fill
|
|
|
|
in instance properties with the appropriate values. Attributes which don't
|
|
|
|
evaluate to :py:class:`Field` instances will not be modified.
|
|
|
|
|
|
|
|
Secrets classes retain their behaviour when they're subclassed. This allows
|
|
|
|
the developer to compose env-specific secrets:
|
|
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
|
|
|
|
class DevelopmentSecrets(AppSecrets):
|
|
|
|
secret_key: str = LiteralField.new('thisisntsecure')
|
|
|
|
|
|
|
|
In this case, the ``secret_key`` field gets overloaded, while all the
|
|
|
|
others remain as declared in ``AppSecrets``.
|
|
|
|
|
|
|
|
:param parent: The parent :py:class:`Secrets` subclass or ``None``.
|
|
|
|
"""
|
2024-01-18 20:28:55 +00:00
|
|
|
|
|
|
|
#: Sentinel for unresolved dependency.
|
|
|
|
UNRESOLVED_DEPENDENCY: list[typing.Any] = []
|
|
|
|
|
2024-01-04 19:30:54 +00:00
|
|
|
__secrets_fields__: TFields
|
|
|
|
|
|
|
|
def __init__(self, parent: Secrets | None = None):
|
|
|
|
self.__secrets_parent__ = parent
|
|
|
|
|
|
|
|
self.__secrets_data__: dict[str, typing.Any] = {}
|
|
|
|
|
|
|
|
for field_name, field in self.__secrets_fields__.items():
|
|
|
|
self.__secrets_data__[field_name] = getattr(self, field_name)
|
2024-01-18 20:28:55 +00:00
|
|
|
|
|
|
|
def resolve_dependency(self,
|
|
|
|
name: str,
|
|
|
|
*,
|
|
|
|
include_parents: bool = True) -> typing.Any:
|
|
|
|
"""
|
|
|
|
Resolve a dependency field identified by *name* and return its value.
|
|
|
|
returns :py:attr:`keep_it_secret.Secrets.UNRESOLVED_DEPENDENCY` if
|
|
|
|
the value can't be resolved.
|
|
|
|
|
|
|
|
:param include_parents: Recursively include parents, if any.
|
|
|
|
"""
|
|
|
|
if name in self.__secrets_fields__:
|
|
|
|
return getattr(self, name)
|
|
|
|
|
|
|
|
if include_parents is True and self.__secrets_parent__ is not None:
|
|
|
|
return self.__secrets_parent__.resolve_dependency(
|
|
|
|
name, include_parents=include_parents,
|
|
|
|
)
|
|
|
|
|
|
|
|
return self.UNRESOLVED_DEPENDENCY
|