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.
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
- Repository: https://gitlab.com/Kencho1/armonia
- Issues: https://gitlab.com/Kencho1/armonia/-/issues
- Changelog: CHANGELOG.md
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a4dda04223ef11eaa623416110595d9d377f1f5447238e6acb47d016dec8fc8
|
|
| MD5 |
4f7a6f63b3a9f0124f934c77604a3e31
|
|
| BLAKE2b-256 |
7b31245bbbef475ec178502d9858e209da87226086236519d409885b365525f3
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e4c9f292e1dac275d6036e0134050b92cd4b3803b34e18a457510bd9d8a0dab
|
|
| MD5 |
1e03c44b652f9bcffcc0ad2819b22305
|
|
| BLAKE2b-256 |
bc94169104d19e50f158e3d3686cb06b3d453fe9555aaa93c1aab233b2d2ecf2
|