Marco Ricci commited on 2024-06-22 21:19:30
              Zeige 2 geänderte Dateien mit 184 Einfügungen und 0 Löschungen.
            
In particular, include type guard functions here as well.
| ... | ... | 
                      @@ -0,0 +1,133 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: MIT  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +"""Common typing declarations for the parent module.  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        +"""  | 
                    |
| 8 | 
                        +  | 
                    |
| 9 | 
                        +from __future__ import annotations  | 
                    |
| 10 | 
                        +  | 
                    |
| 11 | 
                        +from typing import Any, NotRequired, Required, TypedDict, TypeGuard  | 
                    |
| 12 | 
                        +  | 
                    |
| 13 | 
                        +import derivepassphrase  | 
                    |
| 14 | 
                        +  | 
                    |
| 15 | 
                        +__author__ = derivepassphrase.__author__  | 
                    |
| 16 | 
                        +__version__ = derivepassphrase.__version__  | 
                    |
| 17 | 
                        +  | 
                    |
| 18 | 
                        +class VaultConfigGlobalSettings(TypedDict, total=False):  | 
                    |
| 19 | 
                        + r"""Configuration for vault: global settings.  | 
                    |
| 20 | 
                        +  | 
                    |
| 21 | 
                        + Attributes:  | 
                    |
| 22 | 
                        + key:  | 
                    |
| 23 | 
                        + The base64-encoded ssh public key to use, overriding the  | 
                    |
| 24 | 
                        + master passphrase. Optional.  | 
                    |
| 25 | 
                        + phrase:  | 
                    |
| 26 | 
                        + The master passphrase. Optional.  | 
                    |
| 27 | 
                        +  | 
                    |
| 28 | 
                        + """  | 
                    |
| 29 | 
                        + key: NotRequired[str]  | 
                    |
| 30 | 
                        + phrase: NotRequired[str]  | 
                    |
| 31 | 
                        +  | 
                    |
| 32 | 
                        +  | 
                    |
| 33 | 
                        +class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False):  | 
                    |
| 34 | 
                        + r"""Configuration for vault: services settings.  | 
                    |
| 35 | 
                        +  | 
                    |
| 36 | 
                        + Attributes:  | 
                    |
| 37 | 
                        + notes:  | 
                    |
| 38 | 
                        + Optional notes for this service, to display to the user when  | 
                    |
| 39 | 
                        + generating the passphrase.  | 
                    |
| 40 | 
                        + length:  | 
                    |
| 41 | 
                        + Desired passphrase length.  | 
                    |
| 42 | 
                        + repeat:  | 
                    |
| 43 | 
                        + The maximum number of immediate character repetitions  | 
                    |
| 44 | 
                        + allowed in the passphrase. Disabled if set to 0.  | 
                    |
| 45 | 
                        + lower:  | 
                    |
| 46 | 
                        + Optional constraint on ASCII lowercase characters. If  | 
                    |
| 47 | 
                        + positive, include this many lowercase characters  | 
                    |
| 48 | 
                        + somewhere in the passphrase. If 0, avoid lowercase  | 
                    |
| 49 | 
                        + characters altogether.  | 
                    |
| 50 | 
                        + upper:  | 
                    |
| 51 | 
                        + Same as `lower`, but for ASCII uppercase characters.  | 
                    |
| 52 | 
                        + number:  | 
                    |
| 53 | 
                        + Same as `lower`, but for ASCII digits.  | 
                    |
| 54 | 
                        + space:  | 
                    |
| 55 | 
                        + Same as `lower`, but for the space character.  | 
                    |
| 56 | 
                        + dash:  | 
                    |
| 57 | 
                        + Same as `lower`, but for the hyphen-minus and underscore  | 
                    |
| 58 | 
                        + characters.  | 
                    |
| 59 | 
                        + symbol:  | 
                    |
| 60 | 
                        + Same as `lower`, but for all other hitherto unlisted  | 
                    |
| 61 | 
                        + ASCII printable characters (except backquote).  | 
                    |
| 62 | 
                        +  | 
                    |
| 63 | 
                        + """  | 
                    |
| 64 | 
                        + notes: NotRequired[str]  | 
                    |
| 65 | 
                        + length: NotRequired[int]  | 
                    |
| 66 | 
                        + repeat: NotRequired[int]  | 
                    |
| 67 | 
                        + lower: NotRequired[int]  | 
                    |
| 68 | 
                        + upper: NotRequired[int]  | 
                    |
| 69 | 
                        + number: NotRequired[int]  | 
                    |
| 70 | 
                        + space: NotRequired[int]  | 
                    |
| 71 | 
                        + dash: NotRequired[int]  | 
                    |
| 72 | 
                        + symbol: NotRequired[int]  | 
                    |
| 73 | 
                        +  | 
                    |
| 74 | 
                        +  | 
                    |
| 75 | 
                        +_VaultConfig = TypedDict('_VaultConfig',
                       | 
                    |
| 76 | 
                        +                         {'global': NotRequired[VaultConfigGlobalSettings]},
                       | 
                    |
| 77 | 
                        + total=False)  | 
                    |
| 78 | 
                        +class VaultConfig(TypedDict, _VaultConfig, total=False):  | 
                    |
| 79 | 
                        + r"""Configuration for vault.  | 
                    |
| 80 | 
                        +  | 
                    |
| 81 | 
                        + Usually stored as JSON.  | 
                    |
| 82 | 
                        +  | 
                    |
| 83 | 
                        + Attributes:  | 
                    |
| 84 | 
                        + global (NotRequired[VaultConfigGlobalSettings]):  | 
                    |
| 85 | 
                        + Global settings.  | 
                    |
| 86 | 
                        + services (Required[dict[str, VaultConfigServicesSettings]]):  | 
                    |
| 87 | 
                        + Service-specific settings.  | 
                    |
| 88 | 
                        +  | 
                    |
| 89 | 
                        + """  | 
                    |
| 90 | 
                        + services: Required[dict[str, VaultConfigServicesSettings]]  | 
                    |
| 91 | 
                        +  | 
                    |
| 92 | 
                        +def is_vault_config(obj: Any) -> TypeGuard[VaultConfig]:  | 
                    |
| 93 | 
                        + """Check if `obj` is a valid vault config, according to typing.  | 
                    |
| 94 | 
                        +  | 
                    |
| 95 | 
                        + Args:  | 
                    |
| 96 | 
                        + obj: The object to test.  | 
                    |
| 97 | 
                        +  | 
                    |
| 98 | 
                        + Returns:  | 
                    |
| 99 | 
                        + True if this is a vault config, false otherwise.  | 
                    |
| 100 | 
                        +  | 
                    |
| 101 | 
                        + """  | 
                    |
| 102 | 
                        + if not isinstance(obj, dict):  | 
                    |
| 103 | 
                        + return False  | 
                    |
| 104 | 
                        + if 'global' in obj:  | 
                    |
| 105 | 
                        + o_global = obj['global']  | 
                    |
| 106 | 
                        + if not isinstance(o_global, dict):  | 
                    |
| 107 | 
                        + return False  | 
                    |
| 108 | 
                        +        for key in ('key', 'phrase'):
                       | 
                    |
| 109 | 
                        + if key in o_global and not isinstance(o_global[key], str):  | 
                    |
| 110 | 
                        + return False  | 
                    |
| 111 | 
                        + if 'key' in o_global and 'phrase' in o_global:  | 
                    |
| 112 | 
                        + return False  | 
                    |
| 113 | 
                        +    if not isinstance(obj.get('services'), dict):
                       | 
                    |
| 114 | 
                        + return False  | 
                    |
| 115 | 
                        + for sv_name, service in obj['services'].items():  | 
                    |
| 116 | 
                        + if not isinstance(sv_name, str):  | 
                    |
| 117 | 
                        + return False  | 
                    |
| 118 | 
                        + if not isinstance(service, dict):  | 
                    |
| 119 | 
                        + return False  | 
                    |
| 120 | 
                        + for key, value in service.items():  | 
                    |
| 121 | 
                        + match key:  | 
                    |
| 122 | 
                        + case 'notes' | 'phrase' | 'key':  | 
                    |
| 123 | 
                        + if not isinstance(value, str):  | 
                    |
| 124 | 
                        + return False  | 
                    |
| 125 | 
                        + case 'length':  | 
                    |
| 126 | 
                        + if not isinstance(value, int) or value < 1:  | 
                    |
| 127 | 
                        + return False  | 
                    |
| 128 | 
                        + case _:  | 
                    |
| 129 | 
                        + if not isinstance(value, int) or value < 0:  | 
                    |
| 130 | 
                        + return False  | 
                    |
| 131 | 
                        + if 'key' in service and 'phrase' in service:  | 
                    |
| 132 | 
                        + return False  | 
                    |
| 133 | 
                        + return True  | 
                    
| ... | ... | 
                      @@ -0,0 +1,51 @@  | 
                  
| 1 | 
                        +# SPDX-FileCopyrightText: 2024 Marco Ricci <m@the13thletter.info>  | 
                    |
| 2 | 
                        +#  | 
                    |
| 3 | 
                        +# SPDX-License-Identifier: MIT  | 
                    |
| 4 | 
                        +  | 
                    |
| 5 | 
                        +from __future__ import annotations  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        +from typing import Any, cast, TYPE_CHECKING, NamedTuple  | 
                    |
| 8 | 
                        +  | 
                    |
| 9 | 
                        +import derivepassphrase as dpp  | 
                    |
| 10 | 
                        +import pytest  | 
                    |
| 11 | 
                        +  | 
                    |
| 12 | 
                        +@pytest.mark.parametrize(['obj', 'comment'], [  | 
                    |
| 13 | 
                        + (None, 'not a dict'),  | 
                    |
| 14 | 
                        +    ({}, 'missing required keys'),
                       | 
                    |
| 15 | 
                        +    ({'global': None, 'services': {}}, 'bad config value: global'),
                       | 
                    |
| 16 | 
                        +    ({'global': {'key': 123}, 'services': {}},
                       | 
                    |
| 17 | 
                        + 'bad config value: global.key'),  | 
                    |
| 18 | 
                        +    ({'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
                       | 
                    |
| 19 | 
                        + 'incompatible config values: global.key and global.phrase'),  | 
                    |
| 20 | 
                        +    ({'services': None}, 'bad config value: services'),
                       | 
                    |
| 21 | 
                        +    ({'services': {2: {}}}, 'bad config value: services."2"'),
                       | 
                    |
| 22 | 
                        +    ({'services': {'2': 2}}, 'bad config value: services."2"'),
                       | 
                    |
| 23 | 
                        +    ({'services': {'sv': {'notes': False}}},
                       | 
                    |
| 24 | 
                        + 'bad config value: services.sv.notes'),  | 
                    |
| 25 | 
                        +    ({'services': {'sv': {'notes': 'blah blah blah'}}}, ''),
                       | 
                    |
| 26 | 
                        +    ({'services': {'sv': {'length': '200'}}},
                       | 
                    |
| 27 | 
                        + 'bad config value: services.sv.length'),  | 
                    |
| 28 | 
                        +    ({'services': {'sv': {'length': 0.5}}},
                       | 
                    |
| 29 | 
                        + 'bad config value: services.sv.length'),  | 
                    |
| 30 | 
                        +    ({'services': {'sv': {'length': -10}}},
                       | 
                    |
| 31 | 
                        + 'bad config value: services.sv.length'),  | 
                    |
| 32 | 
                        +    ({'services': {'sv': {'upper': -10}}},
                       | 
                    |
| 33 | 
                        + 'bad config value: services.sv.upper'),  | 
                    |
| 34 | 
                        +    ({'global': {'phrase': 'my secret phrase'},
                       | 
                    |
| 35 | 
                        +      'services': {'sv': {'length': 10}}},
                       | 
                    |
| 36 | 
                        + ''),  | 
                    |
| 37 | 
                        +    ({'services': {'sv': {'length': 10, 'phrase': '...'}}}, ''),
                       | 
                    |
| 38 | 
                        +    ({'services': {'sv': {'length': 10, 'key': '...'}}}, ''),
                       | 
                    |
| 39 | 
                        +    ({'services': {'sv': {'upper': 10, 'key': '...'}}}, ''),
                       | 
                    |
| 40 | 
                        +    ({'services': {'sv': {'phrase': 'abc', 'key': '...'}}},
                       | 
                    |
| 41 | 
                        + 'incompatible config values: services.sv.key and services.sv.phrase'),  | 
                    |
| 42 | 
                        +    ({'global': {'phrase': 'abc'},
                       | 
                    |
| 43 | 
                        +      'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
                       | 
                    |
| 44 | 
                        +    ({'global': {'key': '...'},
                       | 
                    |
| 45 | 
                        +      'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
                       | 
                    |
| 46 | 
                        +])  | 
                    |
| 47 | 
                        +def test_200_is_vault_config(obj: Any, comment: str) -> None:  | 
                    |
| 48 | 
                        + assert dpp.types.is_vault_config(obj) == (not comment), (  | 
                    |
| 49 | 
                        + 'failed to complain about: ' + comment if comment  | 
                    |
| 50 | 
                        + else 'failed on valid example'  | 
                    |
| 51 | 
                        + )  | 
                    |
| 0 | 52 |