5 Commits

32 changed files with 148 additions and 35 deletions

View File

@@ -8,7 +8,7 @@ are designed to achieve identical contrast with the accents, and thus any one
of the options can be selected to change the feeling of the palette without of the options can be selected to change the feeling of the palette without
sacrificing readability. sacrificing readability.
![Theme preview](images/repo_preview_four_split.png) ![Theme preview](images/repo_preview_primary.png)
_(Preview of default light and dark theme variants)_ _(Preview of default light and dark theme variants)_
See screenshots for the full set of theme variants in [THEMES](THEMES.md) (also See screenshots for the full set of theme variants in [THEMES](THEMES.md) (also
@@ -44,9 +44,9 @@ lightness level, the contrast between two colors depends on their hue.)*
## Concrete themes ## Concrete themes
![Split view of Alpine and Tundra biomes](images/theme-split-view.png) ![Dark themes](images/dark_themes.png)
*(Light and dark theme splits of Alpine and Tundra biomes)* ![Light themes](images/light_themes.png)
Themes are derived from the `monobiome` palette by varying both the monotone Themes are derived from the `monobiome` palette by varying both the monotone
hue (the "biome") and the extent of the background/foreground lightness (the hue (the "biome") and the extent of the background/foreground lightness (the

View File

@@ -1,3 +1,4 @@
class WLBPosteriorEstimator(PosteriorEstimatorTrainer): class WLBPosteriorEstimator(PosteriorEstimatorTrainer):
""" """
Weighted likelihood bootstrap (WLB) estimator. Weighted likelihood bootstrap (WLB) estimator.
@@ -26,10 +27,12 @@ class WLBPosteriorEstimator(PosteriorEstimatorTrainer):
point-wise basis for a given training run, and we load them later where point-wise basis for a given training run, and we load them later where
we need them in the ``train()`` loop. we need them in the ``train()`` loop.
""" """
theta, x, prior_masks = self.get_simulations(starting_round) theta, x, prior_masks = self.get_simulations(starting_round)
# generate session specific WLB weights to attach point-wise # generate session specific WLB weights to attach point-wise
N = theta.shape[0] N = theta.shape[0]
wlb_z = Exponential(1.0).sample((N,)) wlb_z = Exponential(1.0).sample((N,))
wlb_w = (wlb_z / wlb_z.sum()) * N wlb_w = (wlb_z / wlb_z.sum()) * N

BIN
images/dark_themes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
images/light_themes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

View File

@@ -54,6 +54,7 @@ def register_parser(subparsers: _SubparserType) -> None:
# particularly good measure of perceptual distinction, so we'd prefer the # particularly good measure of perceptual distinction, so we'd prefer the
# former. # former.
parser.add_argument( parser.add_argument(
"-l",
"--l-base", "--l-base",
type=int, type=int,
default=20, default=20,
@@ -82,7 +83,7 @@ def register_parser(subparsers: _SubparserType) -> None:
parser.add_argument( parser.add_argument(
"--term-fg-gap", "--term-fg-gap",
type=int, type=int,
default=60, default=65,
help="Terminal foreground lightness gap (default: 60)", help="Terminal foreground lightness gap (default: 60)",
) )

View File

@@ -16,6 +16,8 @@ L_max: int = parameters.get("L_max", 98)
L_step: int = parameters.get("L_step", 5) L_step: int = parameters.get("L_step", 5)
L_points: list[int] = list(range(L_min, L_max+1)) L_points: list[int] = list(range(L_min, L_max+1))
# L-space just affects accuracy of chroma max
L_space = np.arange(0, 100 + L_step, L_step) L_space = np.arange(0, 100 + L_step, L_step)
monotone_C_map = parameters.get("monotone_C_map", {}) monotone_C_map = parameters.get("monotone_C_map", {})

View File

@@ -1,6 +1,8 @@
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from coloraide import Color
from monobiome.palette import compute_hlc_map
from monobiome.constants import ( from monobiome.constants import (
h_map, h_map,
L_space, L_space,
@@ -52,8 +54,14 @@ def plot_hue_chroma_bounds() -> None:
fig.subplots_adjust(top=0.9) fig.subplots_adjust(top=0.9)
handles, labels = axes[-1].get_legend_handles_labels() handles, labels = axes[-1].get_legend_handles_labels()
unique = dict(zip(labels, handles)) unique = dict(zip(labels, handles, strict=True))
fig.legend(unique.values(), unique.keys(), loc='lower center', bbox_to_anchor=(0.5, -0.06), ncol=3) fig.legend(
unique.values(),
unique.keys(),
loc='lower center',
bbox_to_anchor=(0.5, -0.06),
ncol=3
)
plt.suptitle("$C^*$ curves for hue groups") plt.suptitle("$C^*$ curves for hue groups")
plt.show() plt.show()
@@ -87,11 +95,12 @@ def plot_hue_chroma_star() -> None:
fig.show() fig.show()
def palette_image(palette, cell_size=40, keys=None): def palette_image(
if keys is None: palette: dict[str, dict[int, str]],
names = list(palette.keys()) cell_size: int = 40,
else: keys: list[str] | None = None
names = keys ) -> None:
names = list(palette.keys()) if keys is None else keys
row_count = len(names) row_count = len(names)
col_counts = [len(palette[n]) for n in names] col_counts = [len(palette[n]) for n in names]
@@ -117,8 +126,14 @@ def palette_image(palette, cell_size=40, keys=None):
return img, names, lightness_keys_per_row, cell_size, max_cols return img, names, lightness_keys_per_row, cell_size, max_cols
def show_palette(palette, cell_size=40, keys=None): def show_palette(
img, names, keys, cell_size, max_cols = palette_image(palette, cell_size, keys=keys) palette: dict[str, dict[int, str]],
cell_size: int = 40,
keys: list[str] | None = None
) -> None:
img, names, keys, cell_size, max_cols = palette_image(
palette, cell_size, keys=keys
)
fig_w = img.shape[1] / 100 fig_w = img.shape[1] / 100
fig_h = img.shape[0] / 100 fig_h = img.shape[0] / 100
@@ -130,15 +145,12 @@ def show_palette(palette, cell_size=40, keys=None):
ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))] ytick_pos = [(i + 0.5) * cell_size for i in range(len(names))]
ax.set_yticks(ytick_pos) ax.set_yticks(ytick_pos)
ax.set_yticklabels(names) ax.set_yticklabels(names)
ax.set_ylim(img.shape[0], 0) # ensures rows render w/o half-cells
ax.set_ylim(img.shape[0], 0) # ensures rows render correctly without half-cells
plt.show() plt.show()
if __name__ == "__main__": if __name__ == "__main__":
from monobiome.constants import OKLCH_hL_dict
keys = [ keys = [
"alpine", "alpine",
"badlands", "badlands",
@@ -172,5 +184,6 @@ if __name__ == "__main__":
"blue", "blue",
] ]
show_palette(OKLCH_hL_dict, cell_size=25, keys=keys) hlc_map = compute_hlc_map("oklch")
# show_palette(OKLCH_hL_dict, cell_size=1, keys=term_keys) show_palette(hlc_map, cell_size=25, keys=keys)
# show_palette(hlc_map, cell_size=1, keys=term_keys)

View File

@@ -98,7 +98,7 @@ def generate_scheme_groups(
metric_map = { metric_map = {
"wcag": lambda mc,ac: ac.contrast(mc, method='wcag21'), "wcag": lambda mc,ac: ac.contrast(mc, method='wcag21'),
"oklch": lambda mc,ac: mc.distance(ac, space="oklch"), "oklch": oklch_distance,
"lightness": lambda mc,ac: abs(mc.coords()[0]-ac.coords()[0])*100, "lightness": lambda mc,ac: abs(mc.coords()[0]-ac.coords()[0])*100,
} }
@@ -131,6 +131,9 @@ def generate_scheme_groups(
("distance", distance), ("distance", distance),
("l_base", l_base), ("l_base", l_base),
("l_step", l_step), ("l_step", l_step),
("fg_gap", fg_gap),
("grey_gap", grey_gap),
("term_fg_gap", term_fg_gap),
] ]
# note how selection_bg steps up by `l_step`, selection_fg steps down by # note how selection_bg steps up by `l_step`, selection_fg steps down by
@@ -157,7 +160,7 @@ def generate_scheme_groups(
accent_pairs = [ accent_pairs = [
("black", f"f{{{{{biome}.l{l_base}}}}}"), ("black", f"f{{{{{biome}.l{l_base}}}}}"),
("grey", f"f{{{{{biome}.l{l_base+grey_gap}}}}}"), ("grey", f"f{{{{{biome}.l{l_base+grey_gap}}}}}"),
("white", f"f{{{{{biome}.l{l_base+term_fg_gap-l_step}}}}}"), ("white", f"f{{{{{biome}.l{l_base+term_fg_gap-2*l_step}}}}}"),
] ]
for color_name, mb_accent in accent_color_map.items(): for color_name, mb_accent in accent_color_map.items():
aL = int(100*accent_colors[mb_accent].coords()[0]) aL = int(100*accent_colors[mb_accent].coords()[0])
@@ -184,29 +187,42 @@ def generate_scheme(
term_color_map: dict[str, str], term_color_map: dict[str, str],
vim_color_map: dict[str, str], vim_color_map: dict[str, str],
) -> str: ) -> str:
l_sys = l_base
l_app = l_base + l_step
term_bright_offset = 10
# negate gaps if mode is light
if mode == "light":
l_step *= -1
fg_gap *= -1
grey_gap *= -1
term_fg_gap *= -1
term_bright_offset *= -1
meta, _, mt, ac = generate_scheme_groups( meta, _, mt, ac = generate_scheme_groups(
mode, biome, metric, distance, mode, biome, metric, distance,
l_base, l_step, l_sys, l_step,
fg_gap, grey_gap, term_fg_gap, fg_gap, grey_gap, term_fg_gap,
full_color_map full_color_map
) )
_, term, _, term_norm_ac = generate_scheme_groups( _, term, _, term_norm_ac = generate_scheme_groups(
mode, biome, metric, distance, mode, biome, metric, distance,
l_base + l_step, l_step, l_app, l_step,
fg_gap, grey_gap, term_fg_gap, fg_gap, grey_gap, term_fg_gap,
term_color_map term_color_map
) )
_, _, _, term_bright_ac = generate_scheme_groups( _, _, _, term_bright_ac = generate_scheme_groups(
mode, biome, metric, distance, mode, biome, metric, distance,
l_base + l_step + 10, l_step, l_app + term_bright_offset, l_step,
fg_gap, grey_gap, term_fg_gap, fg_gap, grey_gap, term_fg_gap,
term_color_map term_color_map
) )
_, _, vim_mt, vim_ac = generate_scheme_groups( _, _, vim_mt, vim_ac = generate_scheme_groups(
mode, biome, metric, distance, mode, biome, metric, distance,
l_base + l_step, l_step, l_app, l_step,
fg_gap, grey_gap, term_fg_gap, fg_gap, grey_gap, term_fg_gap,
vim_color_map vim_color_map
) )

View File

@@ -1,3 +1,4 @@
import math
from types import GenericAlias from types import GenericAlias
from argparse import ArgumentParser, _SubParsersAction from argparse import ArgumentParser, _SubParsersAction
@@ -6,5 +7,29 @@ from coloraide import Color
_SubParsersAction.__class_getitem__ = classmethod(GenericAlias) _SubParsersAction.__class_getitem__ = classmethod(GenericAlias)
_SubparserType = _SubParsersAction[ArgumentParser] _SubparserType = _SubParsersAction[ArgumentParser]
def oklch_distance(mc: Color, ac: Color) -> float: def oklch_distance(xc: Color, yc: Color) -> float:
return mc.distance(ac, space="oklch") """
Compute the distance between two colors in OKLCH space.
Note: `xc` and `yc` are presumed to be OKLCH colors already, such that
`.coords()` yields an `(l, c, h)` triple directly rather than first
requiring conversion. When we can make this assumption, we save roughly an
order of magnitude in runtime.
1. `xc.distance(yc, space="oklch")`: 500k evals takes ~2s
2. This method: 500k evals takes ~0.2s
"""
l1, c1, h1 = xc.coords()
l2, c2, h2 = yc.coords()
rad1 = h1 / 180 * math.pi
rad2 = h2 / 180 * math.pi
x1, y1 = c1 * math.cos(rad1), c1 * math.sin(rad1)
x2, y2 = c2 * math.cos(rad2), c2 * math.sin(rad2)
dx = x1 - x2
dy = y1 - y2
dz = l1 - l2
return (dx**2 + dy**2 + dz**2)**0.5

View File

@@ -18,6 +18,7 @@ dependencies = [
"numpy>=2.3.4", "numpy>=2.3.4",
"pillow>=12.0.0", "pillow>=12.0.0",
"plotly>=6.3.1", "plotly>=6.3.1",
"pyqt5>=5.15.11",
"scipy>=1.16.2", "scipy>=1.16.2",
] ]

View File

@@ -16,6 +16,7 @@ kitty -o allow_remote_control=yes --listen-on "unix:$sock" --title "$title" "$@"
# create a targeted rule for the marked window and resize # create a targeted rule for the marked window and resize
sleep 2 sleep 2
swaymsg "for_window [title=\"$title\"] mark --add $title, floating enable, resize set width ${w} px height ${h} px, move position center" >/dev/null swaymsg "for_window [title=\"$title\"] mark --add $title, floating enable, resize set width ${w} px height ${h} px, move position center" >/dev/null
kitty @ --to "unix:$sock" set-font-size 24
# get the spawned window geometry # get the spawned window geometry
sleep 2 sleep 2

View File

@@ -7,23 +7,25 @@ prefix=${1:-}
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
render_script="$script_dir/render.sh" render_script="$script_dir/render.sh"
#biomes=(alpine) biomes=(alpine badlands chaparral savanna grassland tundra reef heathland moorland)
#modes=(light dark) modes=(light)
biomes=(alpine)
modes=(dark)
for biome in "${biomes[@]}"; do for biome in "${biomes[@]}"; do
for mode in "${modes[@]}"; do for mode in "${modes[@]}"; do
echo "Applying $biome-$mode theme" echo "Applying $biome-$mode theme"
monobiome scheme "$mode" "$biome" \
-d 0.42 \
-l 90 \
-o ~/.config/symconf/groups/theme/monobiome-none.toml
symconf config \ symconf config \
-a kitty,nvim \ -a kitty,nvim \
-m "$mode" \ -m "$mode" \
-s "default-$biome-monobiome" \ -s monobiome \
-T font=Berkeley -T font=Berkeley
sleep 2 sleep 1
echo "Taking screenshot..." echo "Taking screenshot..."
"$render_script" 800 600 "images/render/$prefix-$biome-$mode.png" nvim \ "$render_script" 1920 1440 "images/render/$prefix-$biome-$mode.png" nvim \
+':highlight Cursor blend=100' \ +':highlight Cursor blend=100' \
+':set guicursor=n:block-Cursor' \ +':set guicursor=n:block-Cursor' \
+':silent! setlocal nonumber nocursorline signcolumn=no foldcolumn=no' \ +':silent! setlocal nonumber nocursorline signcolumn=no foldcolumn=no' \

51
uv.lock generated
View File

@@ -622,7 +622,7 @@ wheels = [
[[package]] [[package]]
name = "monobiome" name = "monobiome"
version = "1.4.0" version = "1.3.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "coloraide" }, { name = "coloraide" },
@@ -634,6 +634,7 @@ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "pillow" }, { name = "pillow" },
{ name = "plotly" }, { name = "plotly" },
{ name = "pyqt5" },
{ name = "scipy" }, { name = "scipy" },
] ]
@@ -653,6 +654,7 @@ requires-dist = [
{ name = "numpy", specifier = ">=2.3.4" }, { name = "numpy", specifier = ">=2.3.4" },
{ name = "pillow", specifier = ">=12.0.0" }, { name = "pillow", specifier = ">=12.0.0" },
{ name = "plotly", specifier = ">=6.3.1" }, { name = "plotly", specifier = ">=6.3.1" },
{ name = "pyqt5", specifier = ">=5.15.11" },
{ name = "scipy", specifier = ">=1.16.2" }, { name = "scipy", specifier = ">=1.16.2" },
] ]
@@ -1021,6 +1023,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
] ]
[[package]]
name = "pyqt5"
version = "5.15.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyqt5-qt5" },
{ name = "pyqt5-sip" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" },
{ url = "https://files.pythonhosted.org/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" },
{ url = "https://files.pythonhosted.org/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" },
{ url = "https://files.pythonhosted.org/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" },
]
[[package]]
name = "pyqt5-qt5"
version = "5.15.18"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/90/bf01ac2132400997a3474051dd680a583381ebf98b2f5d64d4e54138dc42/pyqt5_qt5-5.15.18-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:8bb997eb903afa9da3221a0c9e6eaa00413bbeb4394d5706118ad05375684767", size = 39715743, upload-time = "2025-11-09T12:56:42.936Z" },
{ url = "https://files.pythonhosted.org/packages/24/8e/76366484d9f9dbe28e3bdfc688183433a7b82e314216e9b14c89e5fab690/pyqt5_qt5-5.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c656af9c1e6aaa7f59bf3d8995f2fa09adbf6762b470ed284c31dca80d686a26", size = 36798484, upload-time = "2025-11-09T12:56:59.998Z" },
{ url = "https://files.pythonhosted.org/packages/9a/46/ffe177f99f897a59dc237a20059020427bd2d3853d713992b8081933ddfe/pyqt5_qt5-5.15.18-py3-none-manylinux2014_x86_64.whl", hash = "sha256:bf2457e6371969736b4f660a0c153258fa03dbc6a181348218e6f05421682af7", size = 60864590, upload-time = "2025-11-09T12:57:26.724Z" },
]
[[package]]
name = "pyqt5-sip"
version = "12.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1e/4a/195cf4d2a7e1ff480b4cabcd51aa5c0068c03a19a97282317536e4a82e1e/pyqt5_sip-12.17.2.tar.gz", hash = "sha256:7f66565c2a13d34d8ad6aad08e953d355ea3fe466d991d51aa5a0966a5289f05", size = 104246, upload-time = "2025-12-06T13:19:06.821Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/3e/f5c7bc43668147ddc00a1a579f22639dffdbfb9470ce3a5bc1cf27e0d541/pyqt5_sip-12.17.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0bd1a8e59124a90a05f078bceeb9d4d93c3986c349030487c202fffde6612969", size = 124612, upload-time = "2025-12-06T13:18:49.614Z" },
{ url = "https://files.pythonhosted.org/packages/b9/41/63f81a53704425092558f1ec17adbed11787f4322e60a849e0539516b3aa/pyqt5_sip-12.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:55ff374feb4bad783241649c7b946e05d7e83d60b0755526ed8fb25bf54e3408", size = 282364, upload-time = "2025-12-06T13:18:51.179Z" },
{ url = "https://files.pythonhosted.org/packages/b5/cd/2b749a174e61394085d61cafb7dc3c11ddf40307edfb2d71cb9b71b7f320/pyqt5_sip-12.17.2-cp312-cp312-win32.whl", hash = "sha256:45dc6e2121d175fdab1431c448fd3e88c97caf873a33cb65efa2e9ad0056337b", size = 49521, upload-time = "2025-12-06T13:18:53.155Z" },
{ url = "https://files.pythonhosted.org/packages/73/ac/7f6d6a6a4505b251f1174092f09d5611c2ed66602c40d3411d93a1d2a95f/pyqt5_sip-12.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:e3bb16e43afd68dd013228075876cf8f8b1a7d86ba67767dd2c6a97be677c18d", size = 58003, upload-time = "2025-12-06T13:18:52.119Z" },
{ url = "https://files.pythonhosted.org/packages/38/b1/78432c271b2a5477f5fe1ad9eb69cdc482430230b8d552cf5cee393d7862/pyqt5_sip-12.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cfeee3c27f28c091d6a46f8befe9afcfafc76846846bedf1112d403a7299e864", size = 124589, upload-time = "2025-12-06T13:18:54.942Z" },
{ url = "https://files.pythonhosted.org/packages/e8/d9/6451973300f7dffe70476cad7fc4a59ffe08417ee4add6afb3288c91bd85/pyqt5_sip-12.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b5df33e198d5d7cccc8e081f80eb97b8d70100f887362074a029a6c19cb92c8b", size = 282040, upload-time = "2025-12-06T13:18:57.019Z" },
{ url = "https://files.pythonhosted.org/packages/c6/1e/241d9ddef5cb1bb3e3b5839b6f8c05ae727e196be82b4646ea4ef9475ef7/pyqt5_sip-12.17.2-cp313-cp313-win32.whl", hash = "sha256:2c0a278b8fc289d34d4e62bbb9ef6da96b45cc9ab3f6886397b1490d2b4a5604", size = 49497, upload-time = "2025-12-06T13:19:00.012Z" },
{ url = "https://files.pythonhosted.org/packages/fd/33/a393163b6299a7e0743fad86fbcb06cf219878fbdd629ee6cb46d2a4d9f7/pyqt5_sip-12.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:7e0d663b583a4d3ac63c9fbade2228de6ee628b44a025f5fd964b97dbbcbebc9", size = 58075, upload-time = "2025-12-06T13:18:58.069Z" },
{ url = "https://files.pythonhosted.org/packages/58/29/b4943def737d3f8876bfd4f9af1909892ae1998099695b3e81870c39aaa7/pyqt5_sip-12.17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6f03c25a18294f2d66befc4f2adf3f35fceba877b937dc8a94783fa7da8b7345", size = 124591, upload-time = "2025-12-06T13:19:02.105Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/e7ac79bb080d4e5a7d7fea50ca7d9231a7ded07e01f24d4e123f089e1630/pyqt5_sip-12.17.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a6d716801d512643b7b1f50dfbdcd16408fe9a6df907d8627b4ad82190604bec", size = 282412, upload-time = "2025-12-06T13:19:03.787Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/b63ee88ffc2e04af90ced17dbe0d774f5f4e51122c13f8118e565707954e/pyqt5_sip-12.17.2-cp314-cp314-win32.whl", hash = "sha256:c617c29524fdcf826e619d77ffd0d6142622f8422adc2608ecc89edd3e605339", size = 50713, upload-time = "2025-12-06T13:19:05.851Z" },
{ url = "https://files.pythonhosted.org/packages/d0/70/efe47083dea494613fc41da55f25c07b4e73bb90c98dee8fe87afbfbc303/pyqt5_sip-12.17.2-cp314-cp314-win_amd64.whl", hash = "sha256:b008755d2222a064ec90c525fce5df3fe9d410371e47c43a21c049e07683b7fb", size = 59620, upload-time = "2025-12-06T13:19:04.829Z" },
]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "8.4.2"