Coverage for src/__init__.py: 100%
94 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-06 02:49 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-06 02:49 +0000
1"""Root module."""
3__author__ = "Mandar Patil (mandarons@pm.me)"
5import logging
6import os
7import sys
8import warnings
10from ruamel.yaml import YAML
12DEFAULT_ROOT_DESTINATION = "./icloud"
13DEFAULT_DRIVE_DESTINATION = "drive"
14DEFAULT_PHOTOS_DESTINATION = "photos"
15DEFAULT_RETRY_LOGIN_INTERVAL_SEC = 600 # 10 minutes
16DEFAULT_SYNC_INTERVAL_SEC = 1800 # 30 minutes
17DEFAULT_REQUEST_TIMEOUT_SEC = 30 # 30 seconds
18DEFAULT_CONFIG_FILE_NAME = "config.yaml"
19ENV_ICLOUD_PASSWORD_KEY = "ENV_ICLOUD_PASSWORD"
20ENV_CONFIG_FILE_PATH_KEY = "ENV_CONFIG_FILE_PATH"
21DEFAULT_LOGGER_LEVEL = "info"
22DEFAULT_LOG_FILE_NAME = "icloud.log"
23DEFAULT_CONFIG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), DEFAULT_CONFIG_FILE_NAME)
24# Operator-overridable via ICLOUD_DOCKER_CONFIG_DIR. Default ``/config`` is the
25# in-container mount point users bind their config volume to. The override
26# lets the test suite run on hosts where ``/config`` isn't writable (macOS,
27# sandboxes).
28_CONFIG_DIR = os.environ.get("ICLOUD_DOCKER_CONFIG_DIR", "/config")
29DEFAULT_COOKIE_DIRECTORY = os.path.join(_CONFIG_DIR, "session_data")
31warnings.filterwarnings("ignore", category=DeprecationWarning)
34def read_config(config_path=DEFAULT_CONFIG_FILE_PATH):
35 """Read config file."""
36 if not (config_path and os.path.exists(config_path)):
37 print(f"Config file not found at {config_path}.")
38 return None
39 with open(file=config_path, encoding="utf-8") as config_file:
40 config = YAML().load(config_file)
41 config["app"]["credentials"]["username"] = (
42 config["app"]["credentials"]["username"].strip() if config["app"]["credentials"]["username"] is not None else ""
43 )
44 return config
47def get_logger_config(config):
48 """Get logger config."""
49 logger_config = {}
50 if "logger" not in config["app"]:
51 return None
52 config_app_logger = config["app"]["logger"]
53 logger_config["level"] = (
54 config_app_logger["level"].strip().lower() if "level" in config_app_logger else DEFAULT_LOGGER_LEVEL
55 )
56 logger_config["filename"] = (
57 config_app_logger["filename"].strip().lower() if "filename" in config_app_logger else DEFAULT_LOG_FILE_NAME
58 )
59 return logger_config
62def log_handler_exists(logger, handler_type, **kwargs):
63 """Check for existing log handler."""
64 for handler in logger.handlers:
65 if isinstance(handler, handler_type):
66 if handler_type is logging.FileHandler:
67 if handler.baseFilename.endswith(kwargs["filename"]):
68 return True
69 elif handler_type is logging.StreamHandler:
70 if handler.stream is kwargs["stream"]:
71 return True
72 return False
75class ColorfulConsoleFormatter(logging.Formatter):
76 """Console formatter for log messages."""
78 grey = "\x1b[38;21m"
79 blue = "\x1b[38;5;39m"
80 yellow = "\x1b[38;5;226m"
81 red = "\x1b[38;5;196m"
82 bold_red = "\x1b[31;1m"
83 reset = "\x1b[0m"
85 def __init__(self, fmt):
86 """Construct with defaults."""
87 super().__init__()
88 self.fmt = fmt
89 self.formats = {
90 logging.DEBUG: self.grey + self.fmt + self.reset,
91 logging.INFO: self.blue + self.fmt + self.reset,
92 logging.WARNING: self.yellow + self.fmt + self.reset,
93 logging.ERROR: self.red + self.fmt + self.reset,
94 logging.CRITICAL: self.bold_red + self.fmt + self.reset,
95 }
97 def format(self, record):
98 """Format the record."""
99 log_fmt = self.formats.get(record.levelno)
100 formatter = logging.Formatter(log_fmt)
101 return formatter.format(record)
104def configure_icloudpy_logging():
105 """Configure icloudpy logging to match app logging level."""
106 logger_config = get_logger_config(config=read_config(config_path=os.environ.get(ENV_CONFIG_FILE_PATH_KEY, DEFAULT_CONFIG_FILE_PATH)))
107 if logger_config:
108 level_name = logging.getLevelName(level=logger_config["level"].upper())
110 # Configure icloudpy loggers to use the same level and enable propagation
111 icloudpy_loggers = [
112 logging.getLogger("icloudpy"),
113 logging.getLogger("icloudpy.base"),
114 logging.getLogger("icloudpy.services"),
115 logging.getLogger("icloudpy.services.photos"),
116 ]
117 for icloudpy_logger in icloudpy_loggers:
118 icloudpy_logger.setLevel(level=level_name)
119 # Enable propagation so messages go to root logger handlers
120 icloudpy_logger.propagate = True
121 # Remove any existing handlers to avoid duplicates
122 icloudpy_logger.handlers.clear()
125def get_logger():
126 """Return logger."""
127 logger = logging.getLogger()
128 logger_config = get_logger_config(config=read_config(config_path=os.environ.get(ENV_CONFIG_FILE_PATH_KEY, DEFAULT_CONFIG_FILE_PATH)))
129 if logger_config:
130 level_name = logging.getLevelName(level=logger_config["level"].upper())
131 logger.setLevel(level=level_name)
133 # Create handlers once and add them to root logger
134 file_handler = None
135 console_handler = None
137 if not log_handler_exists(
138 logger=logger,
139 handler_type=logging.FileHandler,
140 filename=logger_config["filename"],
141 ):
142 file_handler = logging.FileHandler(logger_config["filename"])
143 file_handler.setFormatter(
144 logging.Formatter(
145 "%(asctime)s :: %(levelname)s :: %(name)s :: %(filename)s :: %(lineno)d :: %(message)s",
146 ),
147 )
148 logger.addHandler(file_handler)
150 if not log_handler_exists(logger=logger, handler_type=logging.StreamHandler, stream=sys.stdout):
151 console_handler = logging.StreamHandler(sys.stdout)
152 console_handler.setFormatter(
153 ColorfulConsoleFormatter(
154 "%(asctime)s :: %(levelname)s :: %(name)s :: %(filename)s :: %(lineno)d :: %(message)s",
155 ),
156 )
157 logger.addHandler(console_handler)
159 # Configure icloudpy loggers to use the same level and enable propagation
160 icloudpy_loggers = [
161 logging.getLogger("icloudpy"),
162 logging.getLogger("icloudpy.base"),
163 logging.getLogger("icloudpy.services"),
164 logging.getLogger("icloudpy.services.photos"),
165 ]
166 for icloudpy_logger in icloudpy_loggers:
167 icloudpy_logger.setLevel(level=level_name)
168 # Enable propagation so messages go to root logger handlers
169 icloudpy_logger.propagate = True
170 # Remove any existing handlers to avoid duplicates
171 icloudpy_logger.handlers.clear()
172 return logger
175LOGGER = get_logger()