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

1"""

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

3

4 METADATA:

5

6 File: settings.py

7 Project: paperap

8 Created: 2025-03-09

9 Version: 0.0.5

10 Author: Jess Mann

11 Email: jess@jmann.me

12 Copyright (c) 2025 Jess Mann

13

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

15

16 LAST MODIFIED:

17

18 2025-03-09 By Jess Mann

19

20"""

21

22 from __future__ import annotations

23

24 from abc import ABC, abstractmethod

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

26

27 from pydantic import Field, field_validator

28 from pydantic_settings import BaseSettings, SettingsConfigDict

29 from yarl import URL

30

31 from paperap.exceptions import ConfigurationError

32

33

34 class 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

48 class 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)