From 914c7825640c1b528c5fdf8882b15aa010e4bca7 Mon Sep 17 00:00:00 2001 From: Adam Schill Collberg Date: Tue, 17 Jun 2025 11:41:22 +0200 Subject: [PATCH] Remove enum_tools and pydantic_extra_types usage --- python-wrapper/pyproject.toml | 4 +- python-wrapper/src/neo4j_viz/colors.py | 4 +- python-wrapper/src/neo4j_viz/node.py | 2 +- python-wrapper/src/neo4j_viz/options.py | 6 - .../src/neo4j_viz/pydantic_colors.py | 598 ++++++++++++++++++ python-wrapper/src/neo4j_viz/relationship.py | 2 +- .../src/neo4j_viz/visualization_graph.py | 2 +- python-wrapper/tests/test_colors.py | 2 +- python-wrapper/tests/test_pandas.py | 2 +- 9 files changed, 606 insertions(+), 16 deletions(-) create mode 100644 python-wrapper/src/neo4j_viz/pydantic_colors.py diff --git a/python-wrapper/pyproject.toml b/python-wrapper/pyproject.toml index 4287b8c8..5db62fb5 100644 --- a/python-wrapper/pyproject.toml +++ b/python-wrapper/pyproject.toml @@ -33,8 +33,6 @@ keywords = ["graph", "visualization", "neo4j"] dependencies = [ "ipython >=7, <10", "pydantic >=2 , <3", - "pydantic-extra-types >=2, <3", - "enum-tools==0.12.0" ] requires-python = ">=3.9" @@ -147,12 +145,14 @@ select = [ "F", # flake8 "I", # isort ] +exclude = ["src/neo4j_viz/pydantic_colors.py"] [tool.mypy] strict = true exclude = [ '(^build|^\.?venv)', 'build', + 'src/neo4j_viz/pydantic_colors.py', ] plugins = ['pydantic.mypy'] untyped_calls_exclude=["nbconvert"] diff --git a/python-wrapper/src/neo4j_viz/colors.py b/python-wrapper/src/neo4j_viz/colors.py index f11cd21e..d518e215 100644 --- a/python-wrapper/src/neo4j_viz/colors.py +++ b/python-wrapper/src/neo4j_viz/colors.py @@ -2,13 +2,11 @@ from enum import Enum from typing import Any, Union -import enum_tools -from pydantic_extra_types.color import ColorType +from neo4j_viz.pydantic_colors import ColorType ColorsType = Union[dict[Any, ColorType], Iterable[ColorType]] -@enum_tools.documentation.document_enum class ColorSpace(Enum): """ Describes the type of color space used by a color palette. diff --git a/python-wrapper/src/neo4j_viz/node.py b/python-wrapper/src/neo4j_viz/node.py index 15e73191..3548e6a4 100644 --- a/python-wrapper/src/neo4j_viz/node.py +++ b/python-wrapper/src/neo4j_viz/node.py @@ -4,10 +4,10 @@ from pydantic import AliasChoices, AliasGenerator, BaseModel, Field, field_serializer, field_validator from pydantic.alias_generators import to_camel -from pydantic_extra_types.color import Color, ColorType from .node_size import RealNumber from .options import CaptionAlignment +from .pydantic_colors import Color, ColorType NodeIdType = Union[str, int] diff --git a/python-wrapper/src/neo4j_viz/options.py b/python-wrapper/src/neo4j_viz/options.py index cb67bc62..d6702abf 100644 --- a/python-wrapper/src/neo4j_viz/options.py +++ b/python-wrapper/src/neo4j_viz/options.py @@ -4,11 +4,9 @@ from enum import Enum from typing import Any, Optional, Union -import enum_tools.documentation from pydantic import BaseModel, Field, ValidationError, model_validator -@enum_tools.documentation.document_enum class CaptionAlignment(str, Enum): """ The alignment of the caption text for nodes and relationships. @@ -19,7 +17,6 @@ class CaptionAlignment(str, Enum): BOTTOM = "bottom" -@enum_tools.documentation.document_enum class Layout(str, Enum): FORCE_DIRECTED = "forcedirected" HIERARCHICAL = "hierarchical" @@ -33,7 +30,6 @@ class Layout(str, Enum): GRID = "grid" -@enum_tools.documentation.document_enum class Direction(str, Enum): """ The direction in which the layout should be oriented @@ -45,7 +41,6 @@ class Direction(str, Enum): DOWN = "down" -@enum_tools.documentation.document_enum class Packing(str, Enum): """ The packing method to be used @@ -96,7 +91,6 @@ def construct_layout_options(layout: Layout, options: dict[str, Any]) -> Optiona ) -@enum_tools.documentation.document_enum class Renderer(str, Enum): """ The renderer used to render the visualization. diff --git a/python-wrapper/src/neo4j_viz/pydantic_colors.py b/python-wrapper/src/neo4j_viz/pydantic_colors.py new file mode 100644 index 00000000..332004c6 --- /dev/null +++ b/python-wrapper/src/neo4j_viz/pydantic_colors.py @@ -0,0 +1,598 @@ +"""Color definitions are used as per the CSS3 +[CSS Color Module Level 3](http://www.w3.org/TR/css3-color/#svg-color) specification. + +A few colors have multiple names referring to the sames colors, eg. `grey` and `gray` or `aqua` and `cyan`. + +In these cases the _last_ color when sorted alphabetically takes preferences, +eg. `Color((0, 255, 255)).as_named() == 'cyan'` because "cyan" comes after "aqua". +""" + +from __future__ import annotations + +import math +import re +from colorsys import hls_to_rgb, rgb_to_hls +from typing import Any, Callable, Literal, Union, cast + +from pydantic import GetJsonSchemaHandler +from pydantic._internal import _repr +from pydantic.json_schema import JsonSchemaValue +from pydantic_core import CoreSchema, PydanticCustomError, core_schema + +ColorTuple = Union[tuple[int, int, int], tuple[int, int, int, float]] +ColorType = Union[ColorTuple, str, "Color"] +HslColorTuple = Union[tuple[float, float, float], tuple[float, float, float, float]] + + +class RGBA: + """Internal use only as a representation of a color.""" + + __slots__ = "r", "g", "b", "alpha", "_tuple" + + def __init__(self, r: float, g: float, b: float, alpha: float | None): + self.r = r + self.g = g + self.b = b + self.alpha = alpha + + self._tuple: tuple[float, float, float, float | None] = (r, g, b, alpha) + + def __getitem__(self, item: Any) -> Any: + return self._tuple[item] + + +# these are not compiled here to avoid import slowdown, they'll be compiled the first time they're used, then cached +_r_255 = r"(\d{1,3}(?:\.\d+)?)" +_r_comma = r"\s*,\s*" +_r_alpha = r"(\d(?:\.\d+)?|\.\d+|\d{1,2}%)" +_r_h = r"(-?\d+(?:\.\d+)?|-?\.\d+)(deg|rad|turn)?" +_r_sl = r"(\d{1,3}(?:\.\d+)?)%" +r_hex_short = r"\s*(?:#|0x)?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?\s*" +r_hex_long = r"\s*(?:#|0x)?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?\s*" +# CSS3 RGB examples: rgb(0, 0, 0), rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 50%) +r_rgb = rf"\s*rgba?\(\s*{_r_255}{_r_comma}{_r_255}{_r_comma}{_r_255}(?:{_r_comma}{_r_alpha})?\s*\)\s*" +# CSS3 HSL examples: hsl(270, 60%, 50%), hsla(270, 60%, 50%, 0.5), hsla(270, 60%, 50%, 50%) +r_hsl = rf"\s*hsla?\(\s*{_r_h}{_r_comma}{_r_sl}{_r_comma}{_r_sl}(?:{_r_comma}{_r_alpha})?\s*\)\s*" +# CSS4 RGB examples: rgb(0 0 0), rgb(0 0 0 / 0.5), rgb(0 0 0 / 50%), rgba(0 0 0 / 50%) +r_rgb_v4_style = rf"\s*rgba?\(\s*{_r_255}\s+{_r_255}\s+{_r_255}(?:\s*/\s*{_r_alpha})?\s*\)\s*" +# CSS4 HSL examples: hsl(270 60% 50%), hsl(270 60% 50% / 0.5), hsl(270 60% 50% / 50%), hsla(270 60% 50% / 50%) +r_hsl_v4_style = rf"\s*hsla?\(\s*{_r_h}\s+{_r_sl}\s+{_r_sl}(?:\s*/\s*{_r_alpha})?\s*\)\s*" + +# colors where the two hex characters are the same, if all colors match this the short version of hex colors can be used +repeat_colors = {int(c * 2, 16) for c in "0123456789abcdef"} +rads = 2 * math.pi + + +class Color(_repr.Representation): + """Represents a color.""" + + __slots__ = "_original", "_rgba" + + def __init__(self, value: ColorType) -> None: + self._rgba: RGBA + self._original: ColorType + if isinstance(value, (tuple, list)): + self._rgba = parse_tuple(value) + elif isinstance(value, str): + self._rgba = parse_str(value) + elif isinstance(value, Color): + self._rgba = value._rgba + value = value._original + else: + raise PydanticCustomError( + "color_error", + "value is not a valid color: value must be a tuple, list or string", + ) + + # if we've got here value must be a valid color + self._original = value + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + field_schema: dict[str, Any] = {} + field_schema.update(type="string", format="color") + return field_schema + + def original(self) -> ColorType: + """Original value passed to `Color`.""" + return self._original + + def as_named(self, *, fallback: bool = False) -> str: + """Returns the name of the color if it can be found in `COLORS_BY_VALUE` dictionary, + otherwise returns the hexadecimal representation of the color or raises `ValueError`. + + Args: + fallback: If True, falls back to returning the hexadecimal representation of + the color instead of raising a ValueError when no named color is found. + + Returns: + The name of the color, or the hexadecimal representation of the color. + + Raises: + ValueError: When no named color is found and fallback is `False`. + """ + if self._rgba.alpha is not None: + return self.as_hex() + rgb = cast(tuple[int, int, int], self.as_rgb_tuple()) + + if rgb in COLORS_BY_VALUE: + return COLORS_BY_VALUE[rgb] + else: + if fallback: + return self.as_hex() + else: + raise ValueError("no named color found, use fallback=True, as_hex() or as_rgb()") + + def as_hex(self, format: Literal["short", "long"] = "short") -> str: + """Returns the hexadecimal representation of the color. + + Hex string representing the color can be 3, 4, 6, or 8 characters depending on whether the string + a "short" representation of the color is possible and whether there's an alpha channel. + + Returns: + The hexadecimal representation of the color. + """ + values = [float_to_255(c) for c in self._rgba[:3]] + if self._rgba.alpha is not None: + values.append(float_to_255(self._rgba.alpha)) + + as_hex = "".join(f"{v:02x}" for v in values) + if format == "short" and all(c in repeat_colors for c in values): + as_hex = "".join(as_hex[c] for c in range(0, len(as_hex), 2)) + return f"#{as_hex}" + + def as_rgb(self) -> str: + """Color as an `rgb(, , )` or `rgba(, , , )` string.""" + if self._rgba.alpha is None: + return f"rgb({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)})" + else: + return ( + f"rgba({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)}, " + f"{round(self._alpha_float(), 2)})" + ) + + def as_rgb_tuple(self, *, alpha: bool | None = None) -> ColorTuple: + """Returns the color as an RGB or RGBA tuple. + + Args: + alpha: Whether to include the alpha channel. There are three options for this input: + + - `None` (default): Include alpha only if it's set. (e.g. not `None`) + - `True`: Always include alpha. + - `False`: Always omit alpha. + + Returns: + A tuple that contains the values of the red, green, and blue channels in the range 0 to 255. + If alpha is included, it is in the range 0 to 1. + """ + r, g, b = (float_to_255(c) for c in self._rgba[:3]) + if alpha is None and self._rgba.alpha is None or alpha is not None and not alpha: + return r, g, b + else: + return r, g, b, self._alpha_float() + + def as_hsl(self) -> str: + """Color as an `hsl(, , )` or `hsl(, , , )` string.""" + if self._rgba.alpha is None: + h, s, li = self.as_hsl_tuple(alpha=False) # type: ignore + return f"hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%})" + else: + h, s, li, a = self.as_hsl_tuple(alpha=True) # type: ignore + return f"hsl({h * 360:0.0f}, {s:0.0%}, {li:0.0%}, {round(a, 2)})" + + def as_hsl_tuple(self, *, alpha: bool | None = None) -> HslColorTuple: + """Returns the color as an HSL or HSLA tuple. + + Args: + alpha: Whether to include the alpha channel. + + - `None` (default): Include the alpha channel only if it's set (e.g. not `None`). + - `True`: Always include alpha. + - `False`: Always omit alpha. + + Returns: + The color as a tuple of hue, saturation, lightness, and alpha (if included). + All elements are in the range 0 to 1. + + Note: + This is HSL as used in HTML and most other places, not HLS as used in Python's `colorsys`. + """ + h, l, s = rgb_to_hls(self._rgba.r, self._rgba.g, self._rgba.b) # + if alpha is None: + if self._rgba.alpha is None: + return h, s, l + else: + return h, s, l, self._alpha_float() + return (h, s, l, self._alpha_float()) if alpha else (h, s, l) + + def _alpha_float(self) -> float: + return 1 if self._rgba.alpha is None else self._rgba.alpha + + @classmethod + def __get_pydantic_core_schema__( + cls, source: type[Any], handler: Callable[[Any], CoreSchema] + ) -> core_schema.CoreSchema: + return core_schema.with_info_plain_validator_function( + cls._validate, serialization=core_schema.to_string_ser_schema() + ) + + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> Color: + return cls(__input_value) + + def __str__(self) -> str: + return self.as_named(fallback=True) + + def __repr_args__(self) -> _repr.ReprArgs: + return [(None, self.as_named(fallback=True))] + [("rgb", self.as_rgb_tuple())] + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Color) and self.as_rgb_tuple() == other.as_rgb_tuple() + + def __hash__(self) -> int: + return hash(self.as_rgb_tuple()) + + +def parse_tuple(value: tuple[Any, ...]) -> RGBA: + """Parse a tuple or list to get RGBA values. + + Args: + value: A tuple or list. + + Returns: + An `RGBA` tuple parsed from the input tuple. + + Raises: + PydanticCustomError: If tuple is not valid. + """ + if len(value) == 3: + r, g, b = (parse_color_value(v) for v in value) + return RGBA(r, g, b, None) + elif len(value) == 4: + r, g, b = (parse_color_value(v) for v in value[:3]) + return RGBA(r, g, b, parse_float_alpha(value[3])) + else: + raise PydanticCustomError("color_error", "value is not a valid color: tuples must have length 3 or 4") + + +def parse_str(value: str) -> RGBA: + """Parse a string representing a color to an RGBA tuple. + + Possible formats for the input string include: + + * named color, see `COLORS_BY_NAME` + * hex short eg. `fff` (prefix can be `#`, `0x` or nothing) + * hex long eg. `ffffff` (prefix can be `#`, `0x` or nothing) + * `rgb(, , )` + * `rgba(, , , )` + * `transparent` + + Args: + value: A string representing a color. + + Returns: + An `RGBA` tuple parsed from the input string. + + Raises: + ValueError: If the input string cannot be parsed to an RGBA tuple. + """ + value_lower = value.lower() + if value_lower in COLORS_BY_NAME: + r, g, b = COLORS_BY_NAME[value_lower] + return ints_to_rgba(r, g, b, None) + + m = re.fullmatch(r_hex_short, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v * 2, 16) for v in rgb) + alpha = int(a * 2, 16) / 255 if a else None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_hex_long, value_lower) + if m: + *rgb, a = m.groups() + r, g, b = (int(v, 16) for v in rgb) + alpha = int(a, 16) / 255 if a else None + return ints_to_rgba(r, g, b, alpha) + + m = re.fullmatch(r_rgb, value_lower) or re.fullmatch(r_rgb_v4_style, value_lower) + if m: + return ints_to_rgba(*m.groups()) # type: ignore + + m = re.fullmatch(r_hsl, value_lower) or re.fullmatch(r_hsl_v4_style, value_lower) + if m: + return parse_hsl(*m.groups()) # type: ignore + + if value_lower == "transparent": + return RGBA(0, 0, 0, 0) + + raise PydanticCustomError( + "color_error", + "value is not a valid color: string not recognised as a valid color", + ) + + +def ints_to_rgba( + r: int | str, + g: int | str, + b: int | str, + alpha: float | None = None, +) -> RGBA: + """Converts integer or string values for RGB color and an optional alpha value to an `RGBA` object. + + Args: + r: An integer or string representing the red color value. + g: An integer or string representing the green color value. + b: An integer or string representing the blue color value. + alpha: A float representing the alpha value. Defaults to None. + + Returns: + An instance of the `RGBA` class with the corresponding color and alpha values. + """ + return RGBA( + parse_color_value(r), + parse_color_value(g), + parse_color_value(b), + parse_float_alpha(alpha), + ) + + +def parse_color_value(value: int | str, max_val: int = 255) -> float: + """Parse the color value provided and return a number between 0 and 1. + + Args: + value: An integer or string color value. + max_val: Maximum range value. Defaults to 255. + + Raises: + PydanticCustomError: If the value is not a valid color. + + Returns: + A number between 0 and 1. + """ + try: + color = float(value) + except ValueError as e: + raise PydanticCustomError( + "color_error", + "value is not a valid color: color values must be a valid number", + ) from e + if 0 <= color <= max_val: + return color / max_val + else: + raise PydanticCustomError( + "color_error", + "value is not a valid color: color values must be in the range 0 to {max_val}", + {"max_val": max_val}, + ) + + +def parse_float_alpha(value: None | str | float | int) -> float | None: + """Parse an alpha value checking it's a valid float in the range 0 to 1. + + Args: + value: The input value to parse. + + Returns: + The parsed value as a float, or `None` if the value was None or equal 1. + + Raises: + PydanticCustomError: If the input value cannot be successfully parsed as a float in the expected range. + """ + if value is None: + return None + try: + if isinstance(value, str) and value.endswith("%"): + alpha = float(value[:-1]) / 100 + else: + alpha = float(value) + except ValueError as e: + raise PydanticCustomError( + "color_error", + "value is not a valid color: alpha values must be a valid float", + ) from e + + if math.isclose(alpha, 1): + return None + elif 0 <= alpha <= 1: + return alpha + else: + raise PydanticCustomError( + "color_error", + "value is not a valid color: alpha values must be in the range 0 to 1", + ) + + +def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: float | None = None) -> RGBA: + """Parse raw hue, saturation, lightness, and alpha values and convert to RGBA. + + Args: + h: The hue value. + h_units: The unit for hue value. + sat: The saturation value. + light: The lightness value. + alpha: Alpha value. + + Returns: + An instance of `RGBA`. + """ + s_value, l_value = parse_color_value(sat, 100), parse_color_value(light, 100) + + h_value = float(h) + if h_units in {None, "deg"}: + h_value = h_value % 360 / 360 + elif h_units == "rad": + h_value = h_value % rads / rads + else: + # turns + h_value %= 1 + + r, g, b = hls_to_rgb(h_value, l_value, s_value) + return RGBA(r, g, b, parse_float_alpha(alpha)) + + +def float_to_255(c: float) -> int: + """Converts a float value between 0 and 1 (inclusive) to an integer between 0 and 255 (inclusive). + + Args: + c: The float value to be converted. Must be between 0 and 1 (inclusive). + + Returns: + The integer equivalent of the given float value rounded to the nearest whole number. + """ + return round(c * 255) + + +COLORS_BY_NAME = { + "aliceblue": (240, 248, 255), + "antiquewhite": (250, 235, 215), + "aqua": (0, 255, 255), + "aquamarine": (127, 255, 212), + "azure": (240, 255, 255), + "beige": (245, 245, 220), + "bisque": (255, 228, 196), + "black": (0, 0, 0), + "blanchedalmond": (255, 235, 205), + "blue": (0, 0, 255), + "blueviolet": (138, 43, 226), + "brown": (165, 42, 42), + "burlywood": (222, 184, 135), + "cadetblue": (95, 158, 160), + "chartreuse": (127, 255, 0), + "chocolate": (210, 105, 30), + "coral": (255, 127, 80), + "cornflowerblue": (100, 149, 237), + "cornsilk": (255, 248, 220), + "crimson": (220, 20, 60), + "cyan": (0, 255, 255), + "darkblue": (0, 0, 139), + "darkcyan": (0, 139, 139), + "darkgoldenrod": (184, 134, 11), + "darkgray": (169, 169, 169), + "darkgreen": (0, 100, 0), + "darkgrey": (169, 169, 169), + "darkkhaki": (189, 183, 107), + "darkmagenta": (139, 0, 139), + "darkolivegreen": (85, 107, 47), + "darkorange": (255, 140, 0), + "darkorchid": (153, 50, 204), + "darkred": (139, 0, 0), + "darksalmon": (233, 150, 122), + "darkseagreen": (143, 188, 143), + "darkslateblue": (72, 61, 139), + "darkslategray": (47, 79, 79), + "darkslategrey": (47, 79, 79), + "darkturquoise": (0, 206, 209), + "darkviolet": (148, 0, 211), + "deeppink": (255, 20, 147), + "deepskyblue": (0, 191, 255), + "dimgray": (105, 105, 105), + "dimgrey": (105, 105, 105), + "dodgerblue": (30, 144, 255), + "firebrick": (178, 34, 34), + "floralwhite": (255, 250, 240), + "forestgreen": (34, 139, 34), + "fuchsia": (255, 0, 255), + "gainsboro": (220, 220, 220), + "ghostwhite": (248, 248, 255), + "gold": (255, 215, 0), + "goldenrod": (218, 165, 32), + "gray": (128, 128, 128), + "green": (0, 128, 0), + "greenyellow": (173, 255, 47), + "grey": (128, 128, 128), + "honeydew": (240, 255, 240), + "hotpink": (255, 105, 180), + "indianred": (205, 92, 92), + "indigo": (75, 0, 130), + "ivory": (255, 255, 240), + "khaki": (240, 230, 140), + "lavender": (230, 230, 250), + "lavenderblush": (255, 240, 245), + "lawngreen": (124, 252, 0), + "lemonchiffon": (255, 250, 205), + "lightblue": (173, 216, 230), + "lightcoral": (240, 128, 128), + "lightcyan": (224, 255, 255), + "lightgoldenrodyellow": (250, 250, 210), + "lightgray": (211, 211, 211), + "lightgreen": (144, 238, 144), + "lightgrey": (211, 211, 211), + "lightpink": (255, 182, 193), + "lightsalmon": (255, 160, 122), + "lightseagreen": (32, 178, 170), + "lightskyblue": (135, 206, 250), + "lightslategray": (119, 136, 153), + "lightslategrey": (119, 136, 153), + "lightsteelblue": (176, 196, 222), + "lightyellow": (255, 255, 224), + "lime": (0, 255, 0), + "limegreen": (50, 205, 50), + "linen": (250, 240, 230), + "magenta": (255, 0, 255), + "maroon": (128, 0, 0), + "mediumaquamarine": (102, 205, 170), + "mediumblue": (0, 0, 205), + "mediumorchid": (186, 85, 211), + "mediumpurple": (147, 112, 219), + "mediumseagreen": (60, 179, 113), + "mediumslateblue": (123, 104, 238), + "mediumspringgreen": (0, 250, 154), + "mediumturquoise": (72, 209, 204), + "mediumvioletred": (199, 21, 133), + "midnightblue": (25, 25, 112), + "mintcream": (245, 255, 250), + "mistyrose": (255, 228, 225), + "moccasin": (255, 228, 181), + "navajowhite": (255, 222, 173), + "navy": (0, 0, 128), + "oldlace": (253, 245, 230), + "olive": (128, 128, 0), + "olivedrab": (107, 142, 35), + "orange": (255, 165, 0), + "orangered": (255, 69, 0), + "orchid": (218, 112, 214), + "palegoldenrod": (238, 232, 170), + "palegreen": (152, 251, 152), + "paleturquoise": (175, 238, 238), + "palevioletred": (219, 112, 147), + "papayawhip": (255, 239, 213), + "peachpuff": (255, 218, 185), + "peru": (205, 133, 63), + "pink": (255, 192, 203), + "plum": (221, 160, 221), + "powderblue": (176, 224, 230), + "purple": (128, 0, 128), + "red": (255, 0, 0), + "rosybrown": (188, 143, 143), + "royalblue": (65, 105, 225), + "saddlebrown": (139, 69, 19), + "salmon": (250, 128, 114), + "sandybrown": (244, 164, 96), + "seagreen": (46, 139, 87), + "seashell": (255, 245, 238), + "sienna": (160, 82, 45), + "silver": (192, 192, 192), + "skyblue": (135, 206, 235), + "slateblue": (106, 90, 205), + "slategray": (112, 128, 144), + "slategrey": (112, 128, 144), + "snow": (255, 250, 250), + "springgreen": (0, 255, 127), + "steelblue": (70, 130, 180), + "tan": (210, 180, 140), + "teal": (0, 128, 128), + "thistle": (216, 191, 216), + "tomato": (255, 99, 71), + "turquoise": (64, 224, 208), + "violet": (238, 130, 238), + "wheat": (245, 222, 179), + "white": (255, 255, 255), + "whitesmoke": (245, 245, 245), + "yellow": (255, 255, 0), + "yellowgreen": (154, 205, 50), +} + +COLORS_BY_VALUE = {v: k for k, v in COLORS_BY_NAME.items()} diff --git a/python-wrapper/src/neo4j_viz/relationship.py b/python-wrapper/src/neo4j_viz/relationship.py index 5efe845f..fe075990 100644 --- a/python-wrapper/src/neo4j_viz/relationship.py +++ b/python-wrapper/src/neo4j_viz/relationship.py @@ -5,9 +5,9 @@ from pydantic import AliasChoices, AliasGenerator, BaseModel, Field, field_serializer, field_validator from pydantic.alias_generators import to_camel -from pydantic_extra_types.color import Color, ColorType from .options import CaptionAlignment +from .pydantic_colors import Color, ColorType def create_aliases(field_name: str) -> AliasChoices: diff --git a/python-wrapper/src/neo4j_viz/visualization_graph.py b/python-wrapper/src/neo4j_viz/visualization_graph.py index 33a188bc..ff3d5235 100644 --- a/python-wrapper/src/neo4j_viz/visualization_graph.py +++ b/python-wrapper/src/neo4j_viz/visualization_graph.py @@ -5,7 +5,6 @@ from typing import Any, Callable, Hashable, Optional, Union from IPython.display import HTML -from pydantic_extra_types.color import Color, ColorType from .colors import NEO4J_COLORS_CONTINUOUS, NEO4J_COLORS_DISCRETE, ColorSpace, ColorsType from .node import Node, NodeIdType @@ -18,6 +17,7 @@ RenderOptions, construct_layout_options, ) +from .pydantic_colors import Color, ColorType from .relationship import Relationship diff --git a/python-wrapper/tests/test_colors.py b/python-wrapper/tests/test_colors.py index adc13951..4f5c69ca 100644 --- a/python-wrapper/tests/test_colors.py +++ b/python-wrapper/tests/test_colors.py @@ -1,8 +1,8 @@ import pytest -from pydantic_extra_types.color import Color from neo4j_viz import Node, VisualizationGraph from neo4j_viz.colors import NEO4J_COLORS_CONTINUOUS, NEO4J_COLORS_DISCRETE, ColorSpace +from neo4j_viz.pydantic_colors import Color @pytest.mark.parametrize("override", [True, False]) diff --git a/python-wrapper/tests/test_pandas.py b/python-wrapper/tests/test_pandas.py index 7da45bf1..e61d1cb0 100644 --- a/python-wrapper/tests/test_pandas.py +++ b/python-wrapper/tests/test_pandas.py @@ -1,9 +1,9 @@ import pytest from pandas import DataFrame -from pydantic_extra_types.color import Color from neo4j_viz.node import Node from neo4j_viz.pandas import from_dfs +from neo4j_viz.pydantic_colors import Color def test_from_df() -> None: