diff --git a/disagreement/__init__.py b/disagreement/__init__.py index bb78328..25307b9 100644 --- a/disagreement/__init__.py +++ b/disagreement/__init__.py @@ -25,7 +25,11 @@ from .errors import ( HTTPException, GatewayException, AuthenticationError, + Forbidden, + NotFound, ) +from .color import Color +from .utils import utcnow from .enums import GatewayIntent, GatewayOpcode # Export enums from .error_handler import setup_global_error_handler from .hybrid_context import HybridContext diff --git a/disagreement/color.py b/disagreement/color.py new file mode 100644 index 0000000..59d5e8b --- /dev/null +++ b/disagreement/color.py @@ -0,0 +1,50 @@ +"""Simple color helper similar to discord.py's Color class.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Color: + """Represents an RGB color value.""" + + value: int + + def __post_init__(self) -> None: + if not 0 <= self.value <= 0xFFFFFF: + raise ValueError("Color value must be between 0x000000 and 0xFFFFFF") + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> "Color": + """Create a Color from red, green and blue components.""" + if not all(0 <= c <= 255 for c in (r, g, b)): + raise ValueError("RGB components must be between 0 and 255") + return cls((r << 16) + (g << 8) + b) + + @classmethod + def from_hex(cls, value: str) -> "Color": + """Create a Color from a hex string like ``"#ff0000"``.""" + value = value.lstrip("#") + if len(value) != 6: + raise ValueError("Hex string must be in RRGGBB format") + return cls(int(value, 16)) + + @classmethod + def default(cls) -> "Color": + return cls(0) + + @classmethod + def red(cls) -> "Color": + return cls(0xFF0000) + + @classmethod + def green(cls) -> "Color": + return cls(0x00FF00) + + @classmethod + def blue(cls) -> "Color": + return cls(0x0000FF) + + def to_rgb(self) -> tuple[int, int, int]: + return ((self.value >> 16) & 0xFF, (self.value >> 8) & 0xFF, self.value & 0xFF) diff --git a/disagreement/errors.py b/disagreement/errors.py index df42905..7395779 100644 --- a/disagreement/errors.py +++ b/disagreement/errors.py @@ -77,14 +77,19 @@ class RateLimitError(HTTPException): ) -# You can add more specific exceptions as needed, e.g.: -# class NotFound(HTTPException): -# """Raised for 404 Not Found errors.""" -# pass +# Specific HTTP error exceptions -# class Forbidden(HTTPException): -# """Raised for 403 Forbidden errors.""" -# pass + +class NotFound(HTTPException): + """Raised for 404 Not Found errors.""" + + pass + + +class Forbidden(HTTPException): + """Raised for 403 Forbidden errors.""" + + pass class AppCommandError(DisagreementException): diff --git a/disagreement/utils.py b/disagreement/utils.py new file mode 100644 index 0000000..86c2eed --- /dev/null +++ b/disagreement/utils.py @@ -0,0 +1,10 @@ +"""Utility helpers.""" + +from __future__ import annotations + +from datetime import datetime, timezone + + +def utcnow() -> datetime: + """Return the current timezone-aware UTC time.""" + return datetime.now(timezone.utc) diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 0000000..c2a5aa8 --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,13 @@ +from disagreement.color import Color + + +def test_from_rgb(): + color = Color.from_rgb(255, 127, 0) + assert color.value == 0xFF7F00 + assert color.to_rgb() == (255, 127, 0) + + +def test_static_colors(): + assert Color.red().value == 0xFF0000 + assert Color.green().value == 0x00FF00 + assert Color.blue().value == 0x0000FF diff --git a/tests/test_errors.py b/tests/test_errors.py index 244f3d5..7a104ba 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -4,6 +4,8 @@ from disagreement.errors import ( HTTPException, RateLimitError, AppCommandOptionConversionError, + Forbidden, + NotFound, ) @@ -21,3 +23,10 @@ def test_rate_limit_error_inherits_httpexception(): def test_app_command_option_conversion_error(): exc = AppCommandOptionConversionError("bad", option_name="opt", original_value="x") assert "opt" in str(exc) and "x" in str(exc) + + +def test_specific_http_exceptions(): + not_found = NotFound(message="missing", status=404) + forbidden = Forbidden(message="forbidden", status=403) + assert isinstance(not_found, HTTPException) + assert isinstance(forbidden, HTTPException) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..cac52d4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,8 @@ +from datetime import timezone + +from disagreement.utils import utcnow + + +def test_utcnow_timezone(): + now = utcnow() + assert now.tzinfo == timezone.utc