1

I am trying to create a python package which works with secret managers. Depending on which cloud the user is working (azure, gcp or aws) it should create the required secret manager of that particular cloud provider. For azure this is keyvaul and for gcp this is secret manager. To accomodate this I have created a function that can detect in which cloud the user is working. This will be sent to a Factory class that will create the corresponding secret manager, i.e., keyvault or secret manager etc. I have also created an Abstract class _SecretManager that will be used elsewhere in my code to provide a common interface to fetch secrets.

The problem lies within the type hinting the abstract method "_get_secret_manager". To make it work correctly when using mypy I have to add the following type hint: "Union[Keyvault, SecretManagerServiceClient]". But how can I extend this easily when we have say, 100 different cloud providers without having to write each possible return type for "_get_secret_manager"?

from __future__ import annotations

from abc import ABC, abstractmethod
from google.cloud.secretmanager import SecretManagerServiceClient
from azureml.core import Keyvault

class _SecretManager(ABC):
    """ABC Class for Cloud Secret Managers like Keyvault or GCP Secret Manager"""

    @abstractmethod
    def __init__(self) -> None:
        self.manager = self._get_secret_manager()

    @abstractmethod
    def _get_secret_manager(self):
        pass

    @abstractmethod
    def _get_secret(self, name: str):
        pass


class _AzureKeyVault(_SecretManager):
    """_SecretManager Subclass for getting secrets from azure keyvault"""

    def __init__(self) -> None:
        self.manager = self._get_secret_manager()

    def _get_secret_manager(self):
        manager = Keyvault()
        return manager

    def _get_secret(self, name: str) -> str:
        return self.manager.get_secret(name)


class _GCPSecretManager(_SecretManager):
    """_SecretManager Subclass for getting secrets from azure keyvault"""

    def __init__(self) -> None:
        self.manager = self._get_secret_manager()

    def _get_secret_manager(self):
        manager = SecretManagerServiceClient()
        return manager

    def _get_secret(self, name: str) -> str:
        return self.manager.fetch_secret(name) #is actually a different method but for the sake of simpicity used this


class _SecretManagerFactory:
    """Factory for creating _SecretManager"""

    def __init__(self) -> None:
        self._managers = {"azure": _AzureKeyVault}

    def create_secret_manager(self, cloud_provider, **kwargs) -> _SecretManager:
        try:
            manager = self._managers[cloud_provider]
        except KeyError:
            raise ValueError("This manager does not exist")
        return manager(**kwargs)

I have tried the following:

class _SecretManager(ABC):
    """ABC Class for Cloud Secret Managers like Keyvault or GCP Secret Manager"""

    @abstractmethod
    def __init__(self) -> None:
        self.manager = self._get_secret_manager()

    @abstractmethod
    def _get_secret_manager(self) -> _SecretManager:
        pass

    @abstractmethod
    def _get_secret(self, name: str):
        pass

but with this approach mypy will throw an error: "_SecretManager" has no attribute "get_secret"; maybe "_get_secret"? [attr-defined]. Which is kind of logical.

Jens
  • 201
  • 4
  • 6
  • one of newest python (maybe 3.10) allows to type 'or' using `|` (`A | B` <=> `Union[A,B]`). look at here: https://stackoverflow.com/questions/48722835/custom-type-hint-annotation – Paweł Pietraszko Mar 24 '23 at 10:47
  • I am aware of this. But it still requires me to "manually" define A and B between the "|" operator. If I have hundreds of classes it would not be sufficient. – Jens Mar 24 '23 at 11:22

1 Answers1

0

I experimenting a lot with typing module and it's the best result i've got:

from abc import ABC, abstractmethod
from typing import TypeAlias, TypeVar, Generic, Union, Type, NewType


class _SecretManager(ABC):
    
    @abstractmethod
    def __init__(self) -> None:
        ...


class AClass(_SecretManager):
    
    def __init__(self) -> None:
        super().__init__()
        

class BClass(_SecretManager):
    def __init__(self) -> None:
        super().__init__()

class CClass:
    def __init__(self) -> None:
        ...
        

SecretManagerSubclass = TypeVar('SecretManagerSubclass', *_SecretManager.__subclasses__())
"""
it's probably equal to:
SecretManagerSubclass = TypeVar('SecretManagerSubclass',_SecretManager)
"""

def aaa(a: SecretManagerSubclass):
    ...

aaa('click here')

I use VSCode with some extensions and hints for method results are generated automatically. My hints overrides automatically generated hints.