Skip to main content

A Python library for theming facilities

Project description

Armonia

ἁρμονία

Ancient Greek; noun

The principle of order and beauty through balanced relationships.

A joining, joint; harmony, agreement, concord.

A Python library for elegant color theme management with dynamic computed colors and powerful transformation functions.

Python Version License: MIT

Overview

Armonia provides a sophisticated yet simple system for managing color themes in Python applications.

Built on top of polychromos, it offers:

  • Centralized Theme Management: Define all your colors in one place
  • Dynamic Computed Colors: Colors that automatically update when base colors change
  • Color Functions: Comprehensive transformation library (lighter, darker, mix, contrast, etc.)
  • Palette System: Organize colors into named collections
  • Logotype Management: Store and manage logo URIs alongside your theme colors
  • Serialization Support: Save and load themes from JSON/YAML
  • Safety First: Recursion protection and conflict detection
  • Type-Safe: Full type hints and mypy compatibility

Installation

pip install armonia

Or using uv:

uv add armonia

Quick Start

from armonia import Theme
from armonia import colorfunctions as cf
from polychromos.color import HSLColor

# Create a theme
theme = Theme()

# Set base colors
theme.set_color("primary", HSLColor.from_hex("#2563eb"))
theme.set_color("background", HSLColor.from_hex("#ffffff"))
theme.set_color("text", HSLColor.from_hex("#1f2937"))

# Create computed colors that automatically derive from base colors
theme.set_computed_color("primary_light", cf.lighter("primary", 0.2))
theme.set_computed_color("primary_dark", cf.darker("primary", 0.2))
theme.set_computed_color("text_muted", cf.mix("text", "background", 0.4))
theme.set_computed_color("on_primary", cf.contrast("primary"))

# Get colors - they resolve automatically
print(theme.get_color("primary_light").to_css_hex())  # #6b95f1

# Change the base color - computed colors update automatically!
theme.set_color("primary", HSLColor.from_hex("#7c3aed"))
print(theme.get_color("primary_light").to_css_hex())  # Now reflects new primary color

Key Features

1. Dynamic Color Computation

Computed colors are functions that derive their values from other colors in the theme. When you change a base color, all computed colors update automatically:

# Define once
theme.set_color("brand", HSLColor.from_hex("#ff6b6b"))
theme.set_computed_color("brand_hover", cf.darker("brand", 0.1))
theme.set_computed_color("brand_subtle", cf.alpha("brand", 0.3))

# Update anywhere - computed colors follow
theme.set_color("brand", HSLColor.from_hex("#4ecdc4"))
# brand_hover and brand_subtle automatically reflect the new brand color

2. Comprehensive Color Functions

Over 40 built-in transformation functions organized by purpose:

Lightness & Darkness

cf.lighter("color", 0.2)      # Increase lightness
cf.darker("color", 0.2)       # Decrease lightness
cf.brighten("color", 0.3)     # Significantly lighter
cf.dim("color", 0.3)          # Significantly darker

Saturation

cf.saturate("color", 0.2)     # More vivid
cf.desaturate("color", 0.2)   # More gray
cf.muted("color", 0.4)        # Significantly less saturated
cf.grayscale("color")         # Complete desaturation

Mixing & Blending

cf.mix("color1", "color2", 0.5)           # Blend two colors
cf.tint("color", 0.2)                     # Mix with white
cf.shade("color", 0.2)                    # Mix with black
cf.tone("color", 0.2)                     # Mix with gray
cf.softer("color", "background", 0.3)     # Shift toward background

Opacity

cf.alpha("color", 0.5)        # Set opacity
cf.fade("color", 0.3)         # Fade (alias for alpha)

Advanced Transformations

cf.rotate_hue("color", 180)                # Rotate hue by degrees
cf.complementary("color")                  # Opposite on color wheel
cf.invert("color")                         # Invert lightness
cf.contrast("color")                       # Choose black or white for contrast
cf.alias("color")                          # Reference another color

Scaling Functions

# Multiply mode (direct multiplication)
cf.scale_lightness("color", 0.7)          # new = old * 0.7
cf.scale_saturation("color", 0.5)         # new = old * 0.5

# Screen mode (gentler, preserves more original color)
cf.screen_lightness("color", 0.6)         # new = 1 - (1-old) * 0.6
cf.screen_saturation("color", 0.4)        # new = 1 - (1-old) * 0.4

Multi-Step Transformations

# Chain multiple transformations
theme.set_computed_color(
    "accent_complex",
    cf.multi(
        "accent",
        lambda c: c.delta(0, 0, 0.1),    # Lighter
        lambda c: c.delta(0, -0.2, 0),   # Less saturated
    )
)

3. Color Palettes

Organize related colors into named palettes:

# Define a palette
theme.set_palette("primary_scale", [
    "primary_dark",
    "primary",
    "primary_light",
    "primary_bright"
])

# Get all colors in the palette
colors = theme.get_palette("primary_scale")  # Returns List[HSLColor]

# Use for UI scales, gradients, etc.
theme.set_palette("grays", ["gray_900", "gray_700", "gray_500", "gray_300", "gray_100"])

4. Logotype Management

Store and manage logotype URIs in your theme alongside colors:

# Add logotypes with different URI schemes
theme.set_logotype("company_logo", "https://example.com/logo.svg")
theme.set_logotype("dark_logo", "https://example.com/logo-dark.svg")
theme.set_logotype("favicon", "https://example.com/favicon.ico")

# File URIs for local assets
theme.set_logotype("local_logo", "file:///path/to/logo.png")

# Data URIs for inline resources
theme.set_logotype("inline_icon", "data:image/svg+xml,<svg>...</svg>")

# Retrieve logotype URIs
logo_uri = theme.get_logotype("company_logo")  # Returns the URI string
print(logo_uri)  # https://example.com/logo.svg

# Remove logotypes
theme.remove_logotype("old_logo")

# URI validation
theme.set_logotype("invalid", "not-a-uri")  # Raises InvalidURIError

Logotypes accept any valid URI scheme including https://, http://, file://, data:, ftp://, and others. URIs are validated to ensure they follow standard format.

5. Get All Colors

Retrieve and sort all theme colors:

# Get all colors sorted by name (alphabetically)
colors = theme.get_all_colors(sort_by="name")

# Sort by hue (color wheel order)
colors = theme.get_all_colors(sort_by="hue")

# Sort by saturation (most vivid to most muted)
colors = theme.get_all_colors(sort_by="saturation", reverse=True)

# Sort by lightness (dark to light)
colors = theme.get_all_colors(sort_by="lightness")

# Each entry includes name, color, and source
for entry in colors:
    print(f"{entry.name}: {entry.color.to_css_hex()} ({entry.source})")
    # Example: "primary: #2563eb (manual)"
    # Example: "primary_light: #6b95f1 (computed)"

6. Flexible Color Resolution

The get_color() method resolves colors with a smart fallback chain:

# 1. Theme colors (highest priority)
theme.set_color("brand", HSLColor.from_hex("#ff6b6b"))
theme.get_color("brand")

# 2. Computed colors
theme.set_computed_color("brand_light", cf.lighter("brand"))
theme.get_color("brand_light")

# 3. Web colors (CSS/HTML standard colors)
theme.get_color("tomato")      # Falls back to web color
theme.get_color("skyblue")

# 4. Hex colors (direct parsing)
theme.get_color("#3498db")     # Parses as hex

7. Serialization Support

Load themes from dictionaries (compatible with JSON/YAML):

# Export theme to dictionary
theme_dict = {
    "colors": {
        "primary": "#2563eb",
        "secondary": "#7c3aed"
    },
    "computed_colors": {
        "primary_light": {
            "function": "lighter",
            "args": ["primary", 0.15]
        }
    },
    "palettes": {
        "primary_scale": ["primary_dark", "primary", "primary_light"]
    },
    "logotypes": {
        "company_logo": "https://example.com/logo.svg",
        "favicon": "https://example.com/favicon.ico"
    }
}

# Load from dictionary
theme = Theme.from_dict(theme_dict)

# Or load from JSON
import json

with open("theme.json", "r") as f:
    theme = Theme.from_dict(json.load(f))

# Access logotypes
logo_uri = theme.get_logotype("company_logo")

8. Safety & Validation

Armonia protects against common errors:

# Prevents naming conflicts
theme.set_color("red", ...)          # Error: 'red' is a web color
theme.set_color("primary", ...)
theme.set_computed_color("primary", ...) # Error: already exists as manual color

# Detects circular dependencies
theme.set_computed_color("a", cf.lighter("b"))
theme.set_computed_color("b", cf.darker("a"))
theme.get_color("a")  # Raises ColorRecursionError

# Clear error messages
theme.get_color("nonexistent")  # ColorNotFoundError with helpful message

Complete Example

Here's a complete example of a modern design system:

from armonia import Theme
from armonia import colorfunctions as cf
from polychromos.color import HSLColor

# Create theme
theme = Theme()

# Base colors
theme.set_color("primary", HSLColor.from_hex("#2563eb"))
theme.set_color("secondary", HSLColor.from_hex("#7c3aed"))
theme.set_color("accent", HSLColor.from_hex("#f59e0b"))
theme.set_color("background", HSLColor.from_hex("#ffffff"))
theme.set_color("surface", HSLColor.from_hex("#f3f4f6"))
theme.set_color("text", HSLColor.from_hex("#1f2937"))

# Primary variations
theme.set_computed_color("primary_light", cf.lighter("primary", 0.15))
theme.set_computed_color("primary_dark", cf.darker("primary", 0.15))
theme.set_computed_color("primary_muted", cf.muted("primary", 0.4))
theme.set_computed_color("primary_vivid", cf.saturate("primary", 0.2))

# Semantic colors
theme.set_computed_color("on_primary", cf.contrast("primary"))
theme.set_computed_color("on_secondary", cf.contrast("secondary"))
theme.set_computed_color("on_accent", cf.contrast("accent"))

# Surface variations
theme.set_computed_color("surface_light", cf.lighter("surface", 0.05))
theme.set_computed_color("surface_dark", cf.darker("surface", 0.05))

# Text hierarchy
theme.set_computed_color("text_muted", cf.mix("text", "background", 0.4))
theme.set_computed_color("text_subtle", cf.alpha("text", 0.6))

# Define palettes
theme.set_palette("primary_scale", [
    "primary_dark",
    "primary",
    "primary_light",
    "primary_vivid"
])

theme.set_palette("surfaces", [
    "background",
    "surface",
    "surface_light",
    "surface_dark"
])

theme.set_palette("text_hierarchy", [
    "text",
    "text_muted",
    "text_subtle"
])

# Add logotypes
theme.set_logotype("company_logo", "https://example.com/logo.svg")
theme.set_logotype("dark_logo", "https://example.com/logo-dark.svg")
theme.set_logotype("favicon", "https://example.com/favicon.ico")

# Use colors
print(f"Primary: {theme.get_color('primary').to_css_hex()}")
print(f"Primary Light: {theme.get_color('primary_light').to_css_hex()}")
print(f"Text on Primary: {theme.get_color('on_primary').to_css_hex()}")

# Get a palette
primary_colors = theme.get_palette("primary_scale")
for i, color in enumerate(primary_colors):
    print(f"  {i}: {color.to_css_hex()}")

# Get logotypes
print(f"Logo: {theme.get_logotype('company_logo')}")

Integration Examples

Flask/Django (CSS Custom Properties)

from armonia import Theme
from polychromos.color import HSLColor

theme = Theme()
# ... setup theme ...

def generate_css():
    """Generate CSS custom properties from theme."""
    css_vars = [":root {"]

    for entry in theme.get_all_colors():
        css_name = entry.name.replace("_", "-")
        css_value = entry.color.to_css_hex()
        css_vars.append(f"  --color-{css_name}: {css_value};")

    css_vars.append("}")
    return "\n".join(css_vars)

# In your Flask/Django view
@app.route('/theme.css')
def theme_css():
    return generate_css(), 200, {'Content-Type': 'text/css'}

TailwindCSS Integration

import json

def generate_tailwind_colors(theme):
    """Generate Tailwind color configuration."""
    colors = {}
    for entry in theme.get_all_colors():
        colors[entry.name] = entry.color.to_css_hex()

    config = {
        "theme": {
            "extend": {
                "colors": colors
            }
        }
    }

    with open("tailwind.theme.json", "w") as f:
        json.dump(config, f, indent=2)

Terminal/CLI Applications

from armonia import Theme
from polychromos.color import HSLColor

theme = Theme()
# ... setup theme ...

# Get ANSI color codes for terminal output using polychromos
def color_text(text, color_name):
    color = theme.get_color(color_name)
    # Use 24-bit true color ANSI codes for best color accuracy
    ansi_code = color.to_ansi_color(foreground=True, bits=24)
    return f"{ansi_code}{text}\033[0m"

# For backgrounds
def color_background(text, bg_color_name, fg_color_name="white"):
    bg_color = theme.get_color(bg_color_name)
    fg_color = theme.get_color(fg_color_name)
    bg_ansi = bg_color.to_ansi_color(foreground=False, bits=24)
    fg_ansi = fg_color.to_ansi_color(foreground=True, bits=24)
    return f"{bg_ansi}{fg_ansi}{text}\033[0m"

print(color_text("Success!", "success"))
print(color_text("Warning!", "warning"))
print(color_background(" INFO ", "primary", "white"))

Live Example

Check out the interactive example in examples/color-showcase.py:

cd examples
uv run color-showcase.py

Then open http://localhost:5000 to see a live demonstration with:

  • Interactive color pickers
  • Real-time computed color updates
  • Light/dark theme switching
  • All colors panel with sorting options

Development

This project uses uv for dependency management.

Setup

# Clone the repository
git clone https://gitlab.com/Kencho1/armonia.git
cd armonia

# Install dependencies
uv sync

# Install with test dependencies
uv sync --extra test

Testing

# Run tests
uv run pytest

# Run with coverage
uv run pytest --cov=armonia

# Type checking
uv run mypy armonia

# Linting
uv run ruff check armonia

License

MIT License - see LICENSE file for details.

Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

armonia-1.1.1.tar.gz (25.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

armonia-1.1.1-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

File details

Details for the file armonia-1.1.1.tar.gz.

File metadata

  • Download URL: armonia-1.1.1.tar.gz
  • Upload date:
  • Size: 25.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for armonia-1.1.1.tar.gz
Algorithm Hash digest
SHA256 7a4dda04223ef11eaa623416110595d9d377f1f5447238e6acb47d016dec8fc8
MD5 4f7a6f63b3a9f0124f934c77604a3e31
BLAKE2b-256 7b31245bbbef475ec178502d9858e209da87226086236519d409885b365525f3

See more details on using hashes here.

File details

Details for the file armonia-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: armonia-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 16.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for armonia-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8e4c9f292e1dac275d6036e0134050b92cd4b3803b34e18a457510bd9d8a0dab
MD5 1e03c44b652f9bcffcc0ad2819b22305
BLAKE2b-256 bc94169104d19e50f158e3d3686cb06b3d453fe9555aaa93c1aab233b2d2ecf2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page