refactor: centralize ICONS in constants module

- create core/constants.py with centralized ICONS definition
- remove duplicate ICONS definitions from multiple modules
- add emoji alternatives for better terminal support
- update all modules to use centralized constants
- export ICONS from package __init__.py

This eliminates code duplication and provides a single source
of truth for UI icons used throughout the application.
This commit is contained in:
Alexander Minges 2025-07-25 10:18:27 +02:00
parent 0f74e5dab2
commit 93d54ebc61
Signed by: Athemis
SSH key fingerprint: SHA256:TUXshgulbwL+FRYvBNo54pCsI0auROsSEgSvueKbkZ4
5 changed files with 64 additions and 48 deletions

View file

@ -51,6 +51,7 @@ from .core import (
Person, Person,
PrimitiveMetadataField, PrimitiveMetadataField,
) )
from .core.constants import ICONS
from .processing import ( from .processing import (
CitationBuilder, CitationBuilder,
MetadataProcessor, MetadataProcessor,
@ -80,6 +81,8 @@ __all__ = [
"Institution", "Institution",
"License", "License",
"Abstract", "Abstract",
# Constants
"ICONS",
# Metadata fields # Metadata fields
"BaseMetadataField", "BaseMetadataField",
"PrimitiveMetadataField", "PrimitiveMetadataField",

View file

@ -10,6 +10,7 @@ from typing import Any
from rich.console import Console from rich.console import Console
from ..core.constants import ICONS
from ..core.models import Abstract, License from ..core.models import Abstract, License
from .client import APIClient from .client import APIClient
@ -66,9 +67,6 @@ class AbstractProcessor:
Retrieves and processes abstracts from CrossRef and OpenAlex. Retrieves and processes abstracts from CrossRef and OpenAlex.
""" """
# Icons for console output - TODO: should be moved to a constants module
ICONS = {"info": "", "warning": "⚠️", "error": ""}
def __init__(self, api_client: APIClient, console: Console | None = None): def __init__(self, api_client: APIClient, console: Console | None = None):
""" """
Initialize with an APIClient instance. Initialize with an APIClient instance.
@ -98,7 +96,7 @@ class AbstractProcessor:
if license.short in license_ok: if license.short in license_ok:
self.console.print( self.console.print(
f"\n{self.ICONS['info']} License {license.name} allows derivative works. Pulling abstract from CrossRef.", f"\n{ICONS['info']} License {license.name} allows derivative works. Pulling abstract from CrossRef.",
style="info", style="info",
) )
crossref_abstract = self._get_crossref_abstract(doi) crossref_abstract = self._get_crossref_abstract(doi)
@ -106,18 +104,18 @@ class AbstractProcessor:
return Abstract(text=crossref_abstract, source="crossref") return Abstract(text=crossref_abstract, source="crossref")
else: else:
self.console.print( self.console.print(
f"\n{self.ICONS['warning']} No abstract found in CrossRef!", f"\n{ICONS['warning']} No abstract found in CrossRef!",
style="warning", style="warning",
) )
else: else:
if license.name: if license.name:
self.console.print( self.console.print(
f"\n{self.ICONS['info']} License {license.name} does not allow derivative works. Reconstructing abstract from OpenAlex!", f"\n{ICONS['info']} License {license.name} does not allow derivative works. Reconstructing abstract from OpenAlex!",
style="info", style="info",
) )
else: else:
self.console.print( self.console.print(
f"\n{self.ICONS['info']} Custom license does not allow derivative works. Reconstructing abstract from OpenAlex!", f"\n{ICONS['info']} Custom license does not allow derivative works. Reconstructing abstract from OpenAlex!",
style="info", style="info",
) )
@ -126,12 +124,12 @@ class AbstractProcessor:
return Abstract(text=openalex_abstract, source="openalex") return Abstract(text=openalex_abstract, source="openalex")
else: else:
self.console.print( self.console.print(
f"\n{self.ICONS['warning']} No abstract found in OpenAlex!", f"\n{ICONS['warning']} No abstract found in OpenAlex!",
style="warning", style="warning",
) )
self.console.print( self.console.print(
f"\n{self.ICONS['warning']} No abstract found in either CrossRef nor OpenAlex!", f"\n{ICONS['warning']} No abstract found in either CrossRef nor OpenAlex!",
style="warning", style="warning",
) )
return Abstract(text="", source="none") return Abstract(text="", source="none")

View file

@ -23,28 +23,10 @@ from rich.progress import (
from rich.table import Table from rich.table import Table
from rich.theme import Theme from rich.theme import Theme
from .core.constants import ICONS
from .processing.metadata import MetadataProcessor from .processing.metadata import MetadataProcessor
from .utils.validation import normalize_doi, sanitize_filename, validate_email_address from .utils.validation import normalize_doi, sanitize_filename, validate_email_address
# Console icons for user-friendly output
ICONS = {
"success": "", # Simple checkmark
"error": "", # Simple X
"warning": "!", # Simple exclamation
"info": "", # Info symbol
"processing": "", # Three dots
"done": "", # Filled square
"file": "", # Document symbol
"folder": "", # Folder symbol
"clock": "", # Clock symbol
"search": "", # Search symbol
"data": "", # Three lines
"doi": "", # Link symbol
"total": "", # Sum symbol
"save": "", # Save/download arrow
"upload": "", # Upload arrow
}
# Theme configuration for Rich console output # Theme configuration for Rich console output
THEME = Theme( THEME = Theme(
{ {

View file

@ -0,0 +1,43 @@
"""
Constants for doi2dataset.
This module contains shared constants used across the application,
including console icons and other configuration values.
"""
# Console icons for user-friendly output
ICONS = {
# Status indicators
"success": "", # Simple checkmark
"error": "", # Simple X / ❌ (emoji alternative)
"warning": "!", # Simple exclamation / ⚠️ (emoji alternative)
"info": "", # Info symbol / (emoji alternative)
# Process indicators
"processing": "", # Three dots / ⚙️ (emoji alternative)
"done": "", # Filled square
# File/data indicators
"file": "", # Document symbol
"folder": "", # Folder symbol
"save": "", # Save/download arrow
"upload": "", # Upload arrow
# UI elements
"clock": "", # Clock symbol
"search": "", # Search symbol
"data": "", # Three lines
"doi": "", # Link symbol
"total": "", # Sum symbol
}
# Alternative emoji-based icons for better visibility in some terminals
EMOJI_ICONS = {
"success": "",
"error": "",
"warning": "⚠️",
"info": "",
"processing": "⚙️",
"upload": "📤",
"save": "💾",
}
# Default icon set preference
DEFAULT_ICONS = ICONS

View file

@ -16,6 +16,7 @@ from rich.progress import Progress, TaskID
from ..api.client import APIClient from ..api.client import APIClient
from ..api.processors import AbstractProcessor, LicenseProcessor from ..api.processors import AbstractProcessor, LicenseProcessor
from ..core.config import Config from ..core.config import Config
from ..core.constants import ICONS
from ..core.metadata_fields import ( from ..core.metadata_fields import (
CompoundMetadataField, CompoundMetadataField,
ControlledVocabularyMetadataField, ControlledVocabularyMetadataField,
@ -41,17 +42,6 @@ class MetadataProcessor:
building metadata blocks, and optionally uploading the dataset. building metadata blocks, and optionally uploading the dataset.
""" """
# Icons for console output - TODO: should be moved to a constants module
ICONS = {
"processing": "⚙️",
"success": "",
"error": "",
"warning": "⚠️",
"info": "",
"upload": "📤",
"save": "💾",
}
def __init__( def __init__(
self, self,
doi: str, doi: str,
@ -131,7 +121,7 @@ class MetadataProcessor:
dict[str, Any]: The constructed metadata dictionary. dict[str, Any]: The constructed metadata dictionary.
""" """
self.console.print( self.console.print(
f"{self.ICONS['processing']} Processing DOI: {self.doi}", style="info" f"{ICONS['processing']} Processing DOI: {self.doi}", style="info"
) )
data = self._fetch_data() data = self._fetch_data()
@ -148,7 +138,7 @@ class MetadataProcessor:
self._update_progress() self._update_progress()
self.console.print( self.console.print(
f"\n{self.ICONS['success']} Successfully processed: {self.doi}\n", f"\n{ICONS['success']} Successfully processed: {self.doi}\n",
style="success", style="success",
) )
return metadata return metadata
@ -177,14 +167,14 @@ class MetadataProcessor:
if response is None or response.status_code != 201: if response is None or response.status_code != 201:
self.console.print( self.console.print(
f"\n{self.ICONS['error']} Failed to upload to Dataverse: {url}", f"\n{ICONS['error']} Failed to upload to Dataverse: {url}",
style="error", style="error",
) )
raise ValueError(f"Failed to upload to Dataverse: {url}") raise ValueError(f"Failed to upload to Dataverse: {url}")
else: else:
perma = response.json().get("data", {}).get("persistentId", "") perma = response.json().get("data", {}).get("persistentId", "")
self.console.print( self.console.print(
f"{self.ICONS['upload']} Dataset uploaded to: {config.DATAVERSE['dataverse']} with ID {perma}", f"{ICONS['upload']} Dataset uploaded to: {config.DATAVERSE['dataverse']} with ID {perma}",
style="info", style="info",
) )
@ -205,7 +195,7 @@ class MetadataProcessor:
if response is None or response.status_code != 200: if response is None or response.status_code != 200:
self.console.print( self.console.print(
f"\n{self.ICONS['error']} Failed to fetch data for DOI: {self.doi}", f"\n{ICONS['error']} Failed to fetch data for DOI: {self.doi}",
style="error", style="error",
) )
raise ValueError(f"Failed to fetch data for DOI: {self.doi}") raise ValueError(f"Failed to fetch data for DOI: {self.doi}")
@ -238,7 +228,7 @@ class MetadataProcessor:
if not corresponding_authors: if not corresponding_authors:
self.console.print( self.console.print(
f"{self.ICONS['warning']} No corresponding authors explicitly declared; PIs are used as a fallback!", f"{ICONS['warning']} No corresponding authors explicitly declared; PIs are used as a fallback!",
style="warning", style="warning",
) )
pis = self._get_involved_pis(data) pis = self._get_involved_pis(data)
@ -371,7 +361,7 @@ class MetadataProcessor:
return f"<p>This {doc_type} was published on {publication_date} in <i>{journal}</i></p>" return f"<p>This {doc_type} was published on {publication_date} in <i>{journal}</i></p>"
self.console.print( self.console.print(
f"{self.ICONS['warning']} No abstract header added, missing information (journal, publication date and/or document type)", f"{ICONS['warning']} No abstract header added, missing information (journal, publication date and/or document type)",
style="warning", style="warning",
) )
return "" return ""
@ -461,12 +451,12 @@ class MetadataProcessor:
metadata, f, indent=4, ensure_ascii=False, cls=CustomEncoder metadata, f, indent=4, ensure_ascii=False, cls=CustomEncoder
) )
self.console.print( self.console.print(
f"{self.ICONS['save']} Metadata saved in: {self.output_path}", f"{ICONS['save']} Metadata saved in: {self.output_path}",
style="info", style="info",
) )
except Exception as e: except Exception as e:
self.console.print( self.console.print(
f"{self.ICONS['error']} Error saving metadata: {str(e)}\n", f"{ICONS['error']} Error saving metadata: {str(e)}\n",
style="error", style="error",
) )
raise raise