Coverage for src/paperap/settings.py: 92%
63 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 23:40 -0400
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 23:40 -0400
1"""
2----------------------------------------------------------------------------
4METADATA:
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
14----------------------------------------------------------------------------
16LAST MODIFIED:
182025-03-09 By Jess Mann
20"""
22from __future__ import annotations
24from abc import ABC, abstractmethod
25from typing import Annotated, Any, Optional, Self, TypedDict, override
27from pydantic import Field, field_validator
28from pydantic_settings import BaseSettings, SettingsConfigDict
29from yarl import URL
31from paperap.exceptions import ConfigurationError
34class SettingsArgs(TypedDict, total=False):
35 """
36 Arguments for the settings class
37 """
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
48class Settings(BaseSettings):
49 """
50 Settings for the paperap library
51 """
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
61 model_config = SettingsConfigDict(env_prefix="PAPERLESS_")
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")
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
76 # Validate
77 if not isinstance(value, URL):
78 raise ConfigurationError("Base URL must be a string or a yarl.URL object")
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")
84 # Remove trailing slash
85 if value.path.endswith("/"):
86 value = value.with_path(value.path[:-1])
88 return value
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)
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
104 if value < 0:
105 raise ConfigurationError("Timeout must be a positive integer")
106 return value
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")
116 if not self.base_url:
117 raise ConfigurationError("Base URL is required")
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}")
122 return super().model_post_init(__context)