Coverage for src/paperap/settings.py: 92%

63 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-11 21:37 -0400

1""" 

2---------------------------------------------------------------------------- 

3 

4METADATA: 

5 

6File: settings.py 

7 Project: paperap 

8Created: 2025-03-09 

9 Version: 0.0.5 

10Author: Jess Mann 

11Email: jess@jmann.me 

12 Copyright (c) 2025 Jess Mann 

13 

14---------------------------------------------------------------------------- 

15 

16LAST MODIFIED: 

17 

182025-03-09 By Jess Mann 

19 

20""" 

21 

22from __future__ import annotations 

23 

24from abc import ABC, abstractmethod 

25from typing import Annotated, Any, Optional, Self, TypedDict, override 

26 

27from pydantic import Field, field_validator 

28from pydantic_settings import BaseSettings, SettingsConfigDict 

29from yarl import URL 

30 

31from paperap.exceptions import ConfigurationError 

32 

33 

34class SettingsArgs(TypedDict, total=False): 

35 """ 

36 Arguments for the settings class 

37 """ 

38 

39 base_url: str | URL 

40 token: str | None 

41 username: str | None 

42 password: str | None 

43 timeout: int 

44 require_ssl: bool 

45 save_on_write: bool 

46 

47 

48class Settings(BaseSettings): 

49 """ 

50 Settings for the paperap library 

51 """ 

52 

53 token: str | None = None 

54 username: str | None = None 

55 password: str | None = None 

56 base_url: URL 

57 timeout: int = 60 

58 require_ssl: bool = False 

59 save_on_write: bool = True 

60 

61 model_config = SettingsConfigDict(env_prefix="PAPERLESS_") 

62 

63 @field_validator("base_url", mode="before") 

64 @classmethod 

65 def validate_url(cls, value: Any) -> URL: 

66 """Ensure the URL is properly formatted.""" 

67 if not value: 

68 raise ConfigurationError("Base URL is required") 

69 

70 if isinstance(value, str): 

71 try: 

72 value = URL(value) 

73 except ValueError as e: 

74 raise ConfigurationError(f"Invalid URL format: {value}") from e 

75 

76 # Validate 

77 if not isinstance(value, URL): 

78 raise ConfigurationError("Base URL must be a string or a yarl.URL object") 

79 

80 # Make sure the URL has a scheme 

81 if not all([value.scheme, value.host]): 

82 raise ConfigurationError("Base URL must have a scheme and host") 

83 

84 # Remove trailing slash 

85 if value.path.endswith("/"): 

86 value = value.with_path(value.path[:-1]) 

87 

88 return value 

89 

90 @field_validator("timeout", mode="before") 

91 @classmethod 

92 def validate_timeout(cls, value: Any) -> int: 

93 """Ensure the timeout is a positive integer.""" 

94 try: 

95 if isinstance(value, str): 

96 # May raise ValueError 

97 value = int(value) 

98 

99 if not isinstance(value, int): 

100 raise TypeError("Unknown type for timeout") 

101 except ValueError as ve: 

102 raise TypeError(f"Timeout must be an integer. Provided {value=} of type {type(value)}") from ve 

103 

104 if value < 0: 

105 raise ConfigurationError("Timeout must be a positive integer") 

106 return value 

107 

108 @override 

109 def model_post_init(self, __context: Any): 

110 """ 

111 Validate the settings after they have been initialized. 

112 """ 

113 if self.token is None and (self.username is None or self.password is None): 

114 raise ConfigurationError("Provide a token, or a username and password") 

115 

116 if not self.base_url: 

117 raise ConfigurationError("Base URL is required") 

118 

119 if self.require_ssl and self.base_url.scheme != "https": 

120 raise ConfigurationError(f"URL must use HTTPS. Url: {self.base_url}. Scheme: {self.base_url.scheme}") 

121 

122 return super().model_post_init(__context)