diff --git a/noctua/config/constants.py b/noctua/config/constants.py
index 3829f6707447f2afacaca9bef6a82d6e15022ce0..c05fb8bf18c0b3d94f88afb0a704902198e16c65 100644
--- a/noctua/config/constants.py
+++ b/noctua/config/constants.py
@@ -113,13 +113,10 @@ image_number = {v: k for k, v in image_state.items()}
# Directories
############
-DATA_FOLDER_NAME = "data"
-LOG_DIRECTORY_NAME = "log"
-fits_folder = "fits"
-focus_folder = "focus"
-
-# data_folder = "data"
-# log_folder = "log"
+DATA_FOLDER = "data"
+LOG_FOLDER = "log"
+FITS_FOLDER = "fits"
+FOCUS_FOLDER = "focus"
dir_type = {
0: "dark",
@@ -131,21 +128,6 @@ dir_type = {
# Reversing dir_type
dir_number = {v: k for k, v in dir_type.items()}
-############
-# Logging
-############
-
-# Log file naming
-LOG_FILE_EXTENSION = ".log"
-LATEST_LOG_FILENAME = "latest"
-
-# Rotation settings (used by logger.py)
-LOG_ROTATION_TIME_H = 12 # Midday (hour in UTC)
-LOG_ROTATION_TIME_M = 0 # Midday (minute in UTC)
-LOG_ROTATION_TIME_S = 0 # Midday (second in UTC)
-LOG_ROTATION_INTERVAL_DAYS = 1 # Rotate daily
-LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite)
-
############
# FITS
############
@@ -154,6 +136,11 @@ LOG_BACKUP_COUNT = 0 # 0 means keep all backup files (infinite)
imagetyp = "IMAGETYP" # 'Light'
dateobs = "DATE-OBS" # '2021-12-18T05:09:56.163'
-FILE_PREFIX = "OARPAF."
-ext = ".fits" # File extension
-focus_ext = ".foc"
+############
+# EXTENSIONS
+############
+
+FILE_PREFIX = "OARPAF"
+FITS_EXT = "fits" # File extension
+FOCUS_EXT = "foc"
+LOG_EXT = "log"
diff --git a/noctua/sequencer.py b/noctua/sequencer.py
index 249b2c38911e26ba8fcbb42466196a682ac44b45..6a68fbc905728c95fa576f16133bd5b44e6e10df 100755
--- a/noctua/sequencer.py
+++ b/noctua/sequencer.py
@@ -143,9 +143,9 @@ class Sequencer():
full_module_name = f"noctua.templates.{template_name}"
try:
tplmodule = importlib.import_module(full_module_name)
- except ModuleNotFoundError:
+ except ModuleNotFoundError as e:
log.error(
- f"SEQUENCER: Template module '{full_module_name}' not found.")
+ f"SEQUENCER: Template module '{full_module_name}' not found: {e}")
self.error.append(
f"Template module '{full_module_name}' not found.")
continue
diff --git a/noctua/templates/testlamp.py b/noctua/templates/testlamp.py
index d1b8a25b5caf5d688504ad0bd7590d13b47df908..80879024a4796a2137fcd9feeeaf1c45dd4fd3ab 100644
--- a/noctua/templates/testlamp.py
+++ b/noctua/templates/testlamp.py
@@ -5,7 +5,7 @@
from time import sleep
# Third-party modules
-from templates.basetemplate import BaseTemplate
+from .basetemplate import BaseTemplate
# Other templates
from ..config.constants import on_off
diff --git a/noctua/templates/testoutput.py b/noctua/templates/testoutput.py
index 61d6d717eac910463b4378c021321a1358ec416b..15f29ece6af3b2c18895f03e735a2de6ef98bfc6 100644
--- a/noctua/templates/testoutput.py
+++ b/noctua/templates/testoutput.py
@@ -7,7 +7,7 @@ from astropy.time import Time
# Other templates
from ..config.constants import on_off
-from ..directory.structure import foc_path
+from ..utils.structure import foc_path
# from devices import lamp, light
from ..utils.logger import log
from .basetemplate import BaseTemplate
diff --git a/noctua/utils/logger.py b/noctua/utils/logger.py
index 07163bd21b0ff6ce53156da2724f59ec7cde34a9..832f9c930198a44e7258ecf6229fb0202c7836a1 100644
--- a/noctua/utils/logger.py
+++ b/noctua/utils/logger.py
@@ -1,176 +1,64 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python
"""Custom format log"""
# System modules
-import logging
-import logging.handlers
import sys
-from pathlib import Path
-from datetime import datetime, time
+import os # Will be used if log_path only returns a directory
# Third-party modules
-
-# Custom modules
-from ..config.constants import ( LOG_FILE_EXTENSION, LATEST_LOG_FILENAME, LOG_ROTATION_TIME_H,
- LOG_ROTATION_TIME_M, LOG_ROTATION_TIME_S,
- LOG_ROTATION_INTERVAL_DAYS, LOG_BACKUP_COUNT )
-from .structure import get_log_dir_path, get_dated_log_filepath, get_latest_log_filepath
-
-LOG_DIR = get_log_dir_path() # Creates directory if it doesn't exist
-LATEST_LOG_FILEPATH = get_latest_log_filepath()
-
-log = None # Global logger instance
-
-class ColorizingFormatter(logging.Formatter):
- """
- A custom logging formatter that adds ANSI color codes to log level names
- for console output.
- """
-
- # Color codes
- BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
-
- # Log level to color mapping Using bold for emphasis
- LEVEL_COLORS = {
- logging.DEBUG: f"\x1b[3{MAGENTA};1m", # Bold Magenta
- logging.INFO: f"\x1b[3{GREEN};1m", # Bold Green
- logging.WARNING: f"\x1b[3{YELLOW};1m", # Bold Yellow
- logging.ERROR: f"\x1b[3{RED};1m", # Bold Red
- logging.CRITICAL: f"\x1b[4{RED};1m", # Bold Red with Red Background (or just very bold red)
- }
- RESET_SEQ = "\x1b[0m" # Reset all attributes
-
- def __init__(self, fmt=None, datefmt=None, style='%', validate=True, *, defaults=None):
- super().__init__(fmt, datefmt, style, validate, defaults=defaults)
- # For UTC timestamps if needed by the base Formatter
- self.converter = lambda *args: datetime.utcnow().timetuple()
-
-
- def format(self, record):
- s = super().format(record)
-
- levelname_str = record.levelname
- color = self.LEVEL_COLORS.get(record.levelno)
-
- if color:
- padded_levelname = f"{levelname_str:<8}"
-
- if padded_levelname in s:
- colored_levelname = f"{color}{padded_levelname}{self.RESET_SEQ}"
- s = s.replace(padded_levelname, colored_levelname, 1) # Replace only the first occurrence
- else: # Fallback if padded version not found, try raw (less likely to match in typical formats)
- if levelname_str in s:
- colored_levelname = f"{color}{levelname_str}{self.RESET_SEQ}"
- s = s.replace(levelname_str, colored_levelname, 1)
- return s
-
-
-def setup_logger(name="noctua_logger", log_level=logging.DEBUG):
- global log
- if log:
- return log
-
- logger_instance = logging.getLogger(name)
- logger_instance.setLevel(log_level)
-
- if logger_instance.hasHandlers():
- logger_instance.handlers.clear()
-
- log_format_str = "%(asctime)s.%(msecs)03d | %(levelname)-8s | %(message)s (%(module)s.%(funcName)s:%(lineno)d)"
- formatter = logging.Formatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S")
- # Ensure timestamps are in UTC
- formatter.converter = lambda *args: datetime.utcnow().timetuple()
-
- console_handler = logging.StreamHandler(sys.stderr)
- formatter = ColorizingFormatter(log_format_str, datefmt="%Y-%m-%d %H:%M:%S")
- console_handler.setFormatter(formatter)
- logger_instance.addHandler(console_handler)
-
- # --- TimedRotatingFileHandler for LATEST log ---
- # Rotate at midday UTC
- rotation_time_utc = time(
- hour=LOG_ROTATION_TIME_H,
- minute=LOG_ROTATION_TIME_M,
- second=LOG_ROTATION_TIME_S,
- tzinfo=None
+from loguru import logger
+import datetime
+from .structure import log_path
+
+def mylog():
+ "logger function"
+
+ logger.remove() # Remove default handler to prevent duplicate console logs
+
+ time_fmt = "{time:YYYY-MM-DD HH:mm:ss.SSSSSS!UTC} "
+ level_fmt = "{level: <8} "
+ message_fmt = "| {message} "
+ stack_fmt = "({module}.{function}:{line})"
+ fmt = time_fmt + level_fmt + message_fmt + stack_fmt
+
+ # On standard output
+ logger.add(
+ sys.stderr,
+ format=fmt,
+ level="DEBUG"
)
- rotating_file_handler = logging.handlers.TimedRotatingFileHandler(
- filename=LATEST_LOG_FILEPATH,
- when="midnight", # Base rotation type (daily)
- atTime=rotation_time_utc, # Specific time for rotation
- interval=LOG_ROTATION_INTERVAL_DAYS, # Interval (1 for daily)
- backupCount=LOG_BACKUP_COUNT, # 0 means keep all backups
- encoding='utf-8',
- utc=True
+ full_log_file_path = log_path()
+
+ logger.add(
+ full_log_file_path,
+ format=fmt,
+ colorize=True, # For file logs
+ rotation="16:19", # Local time
+ # retention="7 days", # Old logs
+ level="DEBUG"
)
- def custom_namer_astronomical(default_name):
- try:
- # Example: '/.../OARPAF.latest.log.2023-04-16_12-00-00' (if rotated with atTime)
- base_filepath = Path(default_name)
- suffix = base_filepath.suffix # .log or or .2023-04-16_12-00-00
- # Remove base filename and first dot
- date_str_part = base_filepath.name.replace(LATEST_LOG_FILEPATH.name + '.', '')
- date_str_part = date_str_part.split('_')[0] # Takes YYYY-MM-DD part if HH-MM-SS is appended
- rotation_calendar_date = datetime.strptime(date_str_part, "%Y-%m-%d").date()
- astronomical_date_of_log = rotation_calendar_date - timedelta(days=1)
- astro_date_str = astronomical_date_of_log.strftime("%Y-%m-%d")
- return str(get_dated_log_filepath(astro_date_str))
-
- except ValueError:
- # Fallback if date parsing fails
- log.warning(f"Could not parse date from default rotated log name: {default_name}. Using default.")
- return default_name
-
- def simpler_custom_namer(default_name):
-
- base_filepath = Path(default_name)
-
- # Extract the date part from the suffix
- # Suffix is ".YYYY-MM-DD_HH-MM-SS"
- name_parts = base_filepath.name.split(LATEST_LOG_FILEPATH.name + '.')
- if len(name_parts) > 1:
- timestamp_suffix = name_parts[1]
- date_str = timestamp_suffix.split('_')[0] # Get the YYYY-MM-DD part
-
- try:
- rotation_event_date = datetime.strptime(date_str, "%Y-%m-%d").date()
- # The log contains data for the astronomical night *before* this rotation event date.
- log_file_astro_date = rotation_event_date - timedelta(days=1)
- final_date_str = log_file_astro_date.strftime("%Y-%m-%d")
-
- # Construct the desired filename using structure.py for consistency
- return str(get_dated_log_filepath(final_date_str))
- except ValueError:
- pass # Fall through to default if parsing fails
+ # Custom levels
+ logger.level("DEBUG", color="")
+ logger.level("INFO", color="")
- # Fallback to a cleaned-up version of the default name if parsing is tricky
- dir_name = base_filepath.parent
- original_plus_date = base_filepath.name.replace(LOG_FILE_EXTENSION, '') # Remove .log
- # original_plus_date is like OARPAF.latest.YYYY-MM-DD_HH-MM-SS
- cleaned_name = original_plus_date.replace(LATEST_LOG_FILENAME + '.', '') # Becomes OARPAF.YYYY-MM-DD...
- return str(dir_name / (cleaned_name + LOG_FILE_EXTENSION))
+ return logger
- rotating_file_handler.namer = simpler_custom_namer # Use the simpler namer
- rotating_file_handler.setFormatter(formatter)
- logger_instance.addHandler(rotating_file_handler)
-
- log = logger_instance
- log.info(f"Logger '{name}' configured. Current logs to: {LATEST_LOG_FILEPATH}")
- log.info(f"Rotated daily at {rotation_time_utc} UTC. Backup count: {'Infinite' if LOG_BACKUP_COUNT == 0 else LOG_BACKUP_COUNT}.")
- return log
+# This ensures mylog() is called only once.
+log = mylog()
-# Initialize logger on module import
-from datetime import timedelta # Ensure timedelta is imported for namer
-log = setup_logger()
+def main():
+ """Main function"""
-if __name__ == "__main__":
+ # log is already initialized globally
log.debug("This is a debug message.")
log.info("This is an info message.")
log.warning("This is a warning message.")
log.error("This is an error message.")
log.critical("This is a critical message.")
+
+if __name__ == "__main__":
+ main()
diff --git a/noctua/utils/structure.py b/noctua/utils/structure.py
index 3e0e260127c06ca7a5c1ec2adf4d31fabf2ae870..3b895e6a74b9829e21f1b86e3ab3efebf00da858 100644
--- a/noctua/utils/structure.py
+++ b/noctua/utils/structure.py
@@ -14,16 +14,13 @@ from astropy.io import fits
from astropy.time import Time
# Custom modules
-from ..config.constants import (
- DATA_FOLDER_NAME, LOG_DIRECTORY_NAME,
- FILE_PREFIX, LOG_FILE_EXTENSION, LATEST_LOG_FILENAME,
- dateobs, dir_type, ext, fits_folder,
- frame_number, imagetyp, focus_folder, focus_ext
-)
+from ..config.constants import ( DATA_FOLDER, LOG_FOLDER, FITS_FOLDER,
+ FOCUS_FOLDER, FILE_PREFIX, dateobs,
+ dir_type, frame_number, imagetyp,
+ FITS_EXT, FOCUS_EXT, LOG_EXT )
PROJECT_ROOT = Path(__file__).parent.parent.parent
-
def date_folder():
"""Create a date folder string based on astronomical convention
(changes at midday UTC).
@@ -59,7 +56,7 @@ def fits_path(header, dry=False):
Create a fits file path where the file will be stored
"""
- root = PROJECT_ROOT / DATA_FOLDER_NAME / fits_folder
+ root = PROJECT_ROOT / DATA_FOLDER / FITS_FOLDER
date_str = date_folder()
date_path_part = Path(date_str)
@@ -72,34 +69,23 @@ def fits_path(header, dry=False):
return path
-def get_log_dir_path(dry=False):
+def log_path(dry=False):
"""
Returns the Path object for the log directory.
Creates it if it doesn't exist (unless dry=True).
"""
- log_dir = PROJECT_ROOT / DATA_FOLDER_NAME / LOG_DIRECTORY_NAME
- if not dry:
- log_dir.mkdir(parents=True, exist_ok=True)
- return log_dir
-
-def get_dated_log_filepath(date_str, dry=False):
- """
- Constructs a path for an expected date-stamped (rotated) log
- file. date_str: A string like "YYYY-MM-DD"
- """
- log_dir = get_log_dir_path(dry=dry)
- # Filename: OARPAF.YYYY-MM-DD.log
- log_filename = f"{FILE_PREFIX}{date_str}{LOG_FILE_EXTENSION}"
- return log_dir / log_filename
+ path = PROJECT_ROOT / DATA_FOLDER / LOG_FOLDER
+
+ if not dry:
+ path.mkdir(parents=True, exist_ok=True)
-def get_latest_log_filepath(dry=False):
- """
- Constructs the path for the "latest" log file.
- """
- log_dir = get_log_dir_path(dry=dry)
- latest_filename = f"{FILE_PREFIX}{LATEST_LOG_FILENAME}{LOG_FILE_EXTENSION}"
- return log_dir / latest_filename
+ # OARPAF.YYYY-MM-DD.foc
+ outfile = f"{FILE_PREFIX}.{LOG_EXT}"
+ outpath = path / outfile
+ return outpath
+
+ return outpath
def foc_path(timestamp, dry=False):
@@ -107,14 +93,13 @@ def foc_path(timestamp, dry=False):
Create the focus output text file name and its path
"""
- root = PROJECT_ROOT / DATA_FOLDER_NAME / focus_folder
- path = root
+ path = PROJECT_ROOT / DATA_FOLDER / FOCUS_FOLDER
if not dry:
path.mkdir(parents=True, exist_ok=True)
# OARPAF.YYYY-MM-DD.foc
- outfile = f"{FILE_PREFIX}{timestamp}{focus_ext}"
+ outfile = f"{FILE_PREFIX}.{timestamp}.{FOCUS_EXT}"
outpath = path / outfile
return outpath
@@ -131,7 +116,7 @@ def save_filename(infile_path_str):
date_obs_str = header[dateobs] # DATE-OBS from FITS header
name_for_file = Time(date_obs_str).isot
- outfile_name = f"{FILE_PREFIX}{name_for_file}{ext}"
+ outfile_name = f"{FILE_PREFIX}.{name_for_file}.{FITS_EXT}"
outfile = Path(outfile_name)
outdir = fits_path(header) # This already creates the directory
diff --git a/pyproject.toml b/pyproject.toml
index 445eb777b7f851e50d0f857cd9deaddab6fb1c54..49fd104d4dc4ed6f460119afa02d1f600766c8bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -27,6 +27,7 @@ classifiers = [
dependencies = [
"requests",
"astropy",
+ "loguru",
"gnuplotlib", ## If focus.py
"pyvantagepro", # If meteo.py
]