diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml
new file mode 100644
index 0000000..50d4590
--- /dev/null
+++ b/.github/workflows/test_and_build.yml
@@ -0,0 +1,108 @@
+name: Test and Build
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to publish (leave empty to use current version)'
+ required: false
+ type: string
+ publish:
+ description: 'Publish to PyPI'
+ required: false
+ default: false
+ type: boolean
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10", "3.13"]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: "latest"
+
+ - name: Set up Python ${{ matrix.python-version }}
+ run: uv python install ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ uv sync --dev
+
+ - name: Lint with ruff
+ run: |
+ uv run ruff check .
+ uv run ruff format --check .
+
+ - name: Run tests
+ run: |
+ uv run pytest -v
+
+ - name: Upload coverage reports
+ uses: codecov/codecov-action@v4
+ if: matrix.python-version == '3.11'
+ with:
+ file: ./htmlcov/index.html
+ fail_ci_if_error: false
+
+ build:
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+ with:
+ version: "latest"
+
+ - name: Set up Python
+ run: uv python install 3.11
+
+ - name: Update version if specified
+ if: ${{ github.event.inputs.version != '' }}
+ run: |
+ sed -i 's/version = "[^"]*"/version = "${{ github.event.inputs.version }}"/' pyproject.toml
+ echo "Updated version to ${{ github.event.inputs.version }}"
+
+ - name: Build package
+ run: |
+ uv build
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ publish:
+ needs: [test, build]
+ runs-on: ubuntu-latest
+ if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish == 'true'
+ environment: release
+ permissions:
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
+
+ steps:
+ - name: Download build artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist/
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ verbose: true
\ No newline at end of file
diff --git a/README.md b/README.md
index 99a1090..39d11a0 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,17 @@ SHAPE is an app to play Go with AI feedback, specifically designed to point out
This is an experimental project, and is unlikely to ever become very polished.
-## Installation
+## Quick Start
-* Run install.sh to download the models.
-* Run `poetry shell` and then `poetry install` to install the app in a local Python environment.
- * Alternatively, run `pip install .` to install the app in your current Python environment.
+Run the application directly using `uvx`:
-## Usage
+```bash
+uvx goshape
+```
-* Run `shape` to start the app, or use `python shape/main.py`
+The first time you run this, `uv` will automatically download the package, create a virtual environment, and install all dependencies.
+
+When the application starts for the first time, it will check for the required KataGo models in `~/.katrain/`. If they are not found, a dialog will appear to guide you through downloading them.
## Manual
@@ -35,3 +37,16 @@ Note that a move being probable does not mean it is a good move.
You can select multiple heatmaps to get a blended view, where size/number is the average probability, and the color is the average rank (current, target, AI).
+## TODO list from Gemini
+
+Based on a code review, here are some suggested areas for improvement:
+
+### High Impact
+- **User-Friendly Errors (`main.py`):** Show GUI dialogs for errors instead of crashing the application.
+
+### Medium Impact
+- **Refactor `GameNode` (`game_logic.py`):** Extract board state and rule logic into a separate `Board` class to simplify `GameNode` and improve modularity.
+
+### Low Impact
+- **Code Clarity (`game_logic.py`):** Improve code readability.
+
diff --git a/install.sh b/install.sh
deleted file mode 100755
index 8303fdf..0000000
--- a/install.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-curl -L https://github.com/lightvector/KataGo/releases/download/v1.15.0/b18c384nbt-humanv0.bin.gz -o models/katago-human.bin.gz
-curl -L https://media.katagotraining.org/uploaded/networks/models/kata1/kata1-b28c512nbt-s7709128960-d4462231357.bin.gz -o models/katago-28b.bin.gz
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index fd9b860..b9d143c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,29 +1,58 @@
-[tool.poetry]
-name = "shape"
+[project]
+name = "goshape"
version = "0.1.0"
-description = ""
-authors = ["Sander Land"]
+description = "Shape Habits Analysis and Personalized Evaluation"
+authors = [{name = "Sander Land"}]
readme = "README.md"
+requires-python = ">=3.10,<3.14"
+dependencies = [
+ "PySide6>=6.5.0",
+ "pyqtgraph>=0.13.7",
+ "pysgf>=0.9.0",
+ "numpy>=2.1.2",
+ "httpx>=0.25.0",
+]
-[tool.poetry.dependencies]
-python = "^3.10,<3.13"
-PySide6 = "^6.5.0"
-pyqtgraph = "^0.13.7"
-pysgf = "^0.9.0"
-numpy = "^2.1.2"
+[dependency-groups]
+dev = [
+ "pytest>=7.0.0",
+ "pytest-cov>=4.0.0",
+ "ruff>=0.1.0",
+]
-[tool.vulture]
-ignore_names = ["paintEvent", "keyPressEvent", "mousePressEvent"]
+[project.scripts]
+goshape = "shape.main:main"
+
+[tool.hatch.build.targets.wheel]
+packages = ["shape"]
-[tool.black]
+[tool.ruff]
line-length = 120
+target-version = "py310"
-[tool.isort]
-profile = "black"
+[tool.ruff.lint]
+select = [
+ "E", # pycodestyle errors
+ "W", # pycodestyle warnings
+ "F", # pyflakes
+ "I", # isort
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "UP", # pyupgrade
+]
+ignore = [
+ "E501", # line too long, handled by formatter
+]
-[tool.poetry.scripts]
-shape = "shape.main:main"
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
+
+[tool.vulture]
+ignore_names = ["paintEvent", "keyPressEvent", "mousePressEvent"]
[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/shape/game_logic.py b/shape/game_logic.py
index eb31dbe..18bafd9 100644
--- a/shape/game_logic.py
+++ b/shape/game_logic.py
@@ -37,8 +37,8 @@ def sample(
secondary_data_data = secondary_data if secondary_data is not None else self.grid
moves = [
(Move(coords=(col, row)), prob, d)
- for row, (policy_row, secondary_data_row) in enumerate(zip(self.grid, secondary_data_data))
- for col, (prob, d) in enumerate(zip(policy_row, secondary_data_row))
+ for row, (policy_row, secondary_data_row) in enumerate(zip(self.grid, secondary_data_data, strict=False))
+ for col, (prob, d) in enumerate(zip(policy_row, secondary_data_row, strict=False))
if prob > 0
]
if self.pass_prob > 0 and not exclude_pass:
diff --git a/shape/katago/analysis.cfg b/shape/katago/analysis.cfg
new file mode 100644
index 0000000..5355de4
--- /dev/null
+++ b/shape/katago/analysis.cfg
@@ -0,0 +1,215 @@
+# Example config for C++ (non-python) gtp bot
+
+# SEE NOTES ABOUT PERFORMANCE AND MEMORY USAGE IN gtp_example.cfg
+# SEE NOTES ABOUT numSearchThreads AND OTHER IMPORTANT PARAMS BELOW!
+
+# Logs------------------------------------------------------------------------------------
+
+# Where to output log?
+# logFile = analysis.log # Use this instead of logDir to just specify a single file directly
+# logToStderr = true # Echo everything output to log file to stderr as well
+# logAllRequests = false # Log all input lines received to the analysis engine.
+# logAllResponses = false # Log all lines output to stdout from the analysis engine.
+# logSearchInfo = false # Log debug info for every search performed
+
+# Controls the number of moves after the first move in a variation.
+# analysisPVLen = 15
+
+# Report winrates for analysis as (BLACK|WHITE|SIDETOMOVE).
+reportAnalysisWinratesAs = BLACK
+
+# Bot behavior---------------------------------------------------------------------------------------
+
+# Handicap -------------
+
+# Assume that if black makes many moves in a row right at the start of the game, then the game is a handicap game.
+# This is necessary on some servers and for some GUIs and also when initializing from many SGF files, which may
+# set up a handicap games using repeated GTP "play" commands for black rather than GTP "place_free_handicap" commands.
+# However, it may also lead to incorrect undersanding of komi if whiteBonusPerHandicapStone = 1 and a server does NOT
+# have such a practice.
+# Defaults to true! Uncomment and set to false to disable this behavior.
+# assumeMultipleStartingBlackMovesAreHandicap = true
+
+# Passing and cleanup -------------
+
+# Make the bot never assume that its pass will end the game, even if passing would end and "win" under Tromp-Taylor rules.
+# Usually this is a good idea when using it for analysis or playing on servers where scoring may be implemented non-tromp-taylorly.
+# Defaults to true! Uncomment and set to false to disable this.
+conservativePass = true
+
+# When using territory scoring, self-play games continue beyond two passes with special cleanup
+# rules that may be confusing for human players. This option prevents the special cleanup phases from being
+# reachable when using the bot for GTP play.
+# Defaults to true! Uncomment and set to false if you want KataGo to be able to enter special cleanup.
+# For example, if you are testing it against itself, or against another bot that has precisely implemented the rules
+# documented at https://lightvector.github.io/KataGo/rules.html
+# preventCleanupPhase = true
+
+# Search limits-----------------------------------------------------------------------------------
+
+# By default, if NOT specified in an individual request, limit maximum number of root visits per search to this much
+maxVisits = 500
+# If provided, cap search time at this many seconds
+# maxTime = 60
+
+
+# numSearchThreads is the number of threads to use in each MCTS tree search in parallel for any individual position.
+# But NOTE: Analysis engine also specifies max number of POSITIONS to be able to search in parallel via command line
+# argument, -num-analysis-threads.
+
+# Parallelization across positions is more efficient since the threads on different positions operate
+# on different MCTS trees so they don't have to synchronize with each other. Also, multiple threads on the same MCTS
+# tree weakens the search (holding playouts fixed) due to out of date statistics on nodes and suboptimal exploration,
+# although the loss is still quite small for only 2,4,8 threads. So you often want to keep numSearchThreads small,
+# unlike in GTP.
+
+# But obviously you only get the benefit of parallelization across positions when you actually have lots of positions
+# that you are querying at once.
+
+# Therefore:
+# * If you plan to use the analysis engine only for batch processing large numbers of positions,
+# it's preferable to set this to only a small number (e.g. 1,2,4) and use a higher -num-analysis-threads.
+# * But if you sometimes plan to query the analysis engine for single positions, or otherwise in smaller quantities
+# than -num-analysis-threads, or if you plan to be user-interactive such that the response time on some individual
+# analysis requests is important to keep low, then set this to a larger number and use somewhat fewer analysis threads,
+# That way, individual searches complete faster due to having more threads on each one and doing fewer other ones at a time.
+
+# For 19x19 boards, weaker GPUs probably want a TOTAL number of threads (numSearchThreads * num-analysis-threads)
+# between 4 and 32. Mid-tier GPUs probably between 16 and 64. Strong GPUs probably between 32 and 256.
+# But there's no substitute for experimenting and seeing what's best for your hardware and your usage case.
+# Keep in mind that the number of threads you want doesn't necessarily have much to do with how many cores you
+# have on your system, and could easily exceed the number of cores. GPU batching is (usually) the dominant consideration.
+numAnalysisThreads = 12
+numSearchThreads = 4
+
+# nnMaxBatchSize is the max number of positions to send to a single GPU at once. Generally, it should be the case that:
+# (number of GPUs you will use * nnMaxBatchSize) >= (numSearchThreads * num-analysis-threads)
+# That way, when each threads tries to request a GPU eval, your batch size summed across GPUs is large enough to handle them
+# all at once. However, it can be sensible to set this a little smaller if you are limited on GPU memory,
+# too large a number may fail if the GPU doesn't have enough memory.
+nnMaxBatchSize = 96
+
+# Eigen-specific settings--------------------------------------
+# These only apply when using the Eigen (pure CPU) version of KataGo.
+
+# This is the number of CPU threads for evaluating the neural net on the Eigen backend.
+# It defaults to min(numAnalysisThreads * numSearchThreadsPerAnalysisThread, numCPUCores).
+# numEigenThreadsPerModel = X
+
+# Uncomment and set these smaller if you ONLY are going to use the analysis engine for smaller boards (or plan to
+# run multiple instances, with some instances only handling smaller boards). It should improve performance.
+# It may also mean you can use more threads profitably.
+# maxBoardXSizeForNNBuffer = 19
+# maxBoardYSizeForNNBuffer = 19
+
+# TO USE MULTIPLE GPUS:
+# Uncomment and set this to the number of GPUs you have and/or would like to use...
+# AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
+# numNNServerThreadsPerModel = 1
+
+# Other General GPU Settings-------------------------------------------------------------------------------
+
+
+# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
+nnCacheSizePowerOfTwo = 20
+# Size of mutex pool for nnCache is 2 ** this
+nnMutexPoolSizePowerOfTwo = 16
+# Randomize board orientation when running neural net evals?
+nnRandomize = true
+
+# TO USE MULTIPLE GPUS:
+# Set this to the number of GPUs you have and/or would like to use...
+# AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
+# numNNServerThreadsPerModel = 1
+
+
+# CUDA GPU settings--------------------------------------
+# These only apply when using the CUDA version of KataGo.
+
+# IF USING ONE GPU: optionally uncomment and change this if the GPU you want to use turns out to be not device 0
+# cudaDeviceToUse = 0
+
+# IF USING TWO GPUS: Uncomment these two lines (AND set numNNServerThreadsPerModel above):
+# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
+# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
+
+# IF USING THREE GPUS: Uncomment these three lines (AND set numNNServerThreadsPerModel above):
+# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
+# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
+# cudaDeviceToUseThread2 = 2 # change this if the third GPU you want to use turns out to be not device 2
+
+# You can probably guess the pattern if you have four, five, etc. GPUs.
+
+# KataGo will automatically use FP16 or not based on the compute capability of your NVIDIA GPU. If you
+# want to try to force a particular behavior though you can uncomment these lines and change them
+# to "true" or "false". E.g. it's using FP16 but on your card that's giving an error, or it's not using
+# FP16 but you think it should.
+# cudaUseFP16 = auto
+# cudaUseNHWC = auto
+
+# OpenCL GPU settings--------------------------------------
+# These only apply when using the OpenCL version of KataGo.
+
+# Uncomment to tune OpenCL for every board size separately, rather than only the largest possible size
+# openclReTunePerBoardSize = true
+
+# IF USING ONE GPU: optionally uncomment and change this if the best device to use is guessed incorrectly.
+# The default behavior tries to guess the 'best' GPU or device on your system to use, usually it will be a good guess.
+# openclDeviceToUse = 0
+
+# IF USING TWO GPUS: Uncomment these two lines and replace X and Y with the device ids of the devices you want to use.
+# It might NOT be 0 and 1, some computers will have many OpenCL devices. You can see what the devices are when
+# KataGo starts up - it should print or log all the devices it finds.
+# (AND also set numNNServerThreadsPerModel above)
+# openclDeviceToUseThread0 = X
+# openclDeviceToUseThread1 = Y
+
+# IF USING THREE GPUS: Uncomment these three lines and replace X and Y and Z with the device ids of the devices you want to use.
+# It might NOT be 0 and 1 and 2, some computers will have many OpenCL devices. You can see what the devices are when
+# KataGo starts up - it should print or log all the devices it finds.
+# (AND also set numNNServerThreadsPerModel above)
+# openclDeviceToUseThread0 = X
+# openclDeviceToUseThread1 = Y
+# openclDeviceToUseThread2 = Z
+
+# You can probably guess the pattern if you have four, five, etc. GPUs.
+
+
+# Root move selection and biases------------------------------------------------------------------------------
+# Uncomment and edit any of the below values to change them from their default.
+# Not all of these parameters are applicable to analysis, some are only used for actual play
+
+# Temperature for the early game, randomize between chosen moves with this temperature
+# chosenMoveTemperatureEarly = 0.5
+# Decay temperature for the early game by 0.5 every this many moves, scaled with board size.
+# chosenMoveTemperatureHalflife = 19
+# At the end of search after the early game, randomize between chosen moves with this temperature
+# chosenMoveTemperature = 0.10
+# Subtract this many visits from each move prior to applying chosenMoveTemperature
+# (unless all moves have too few visits) to downweight unlikely moves
+# chosenMoveSubtract = 0
+# The same as chosenMoveSubtract but only prunes moves that fall below the threshold, does not affect moves above
+# chosenMovePrune = 1
+
+# Number of symmetries to sample (WITH replacement) and average at the root
+# rootNumSymmetriesToSample = 1
+
+# Using LCB for move selection?
+# useLcbForSelection = true
+# How many stdevs a move needs to be better than another for LCB selection
+# lcbStdevs = 5.0
+# Policy temperature to use for move selection
+# policyTemperature = 1.0
+
+# ROOT POSITIONAL BIASES--------------
+# Uncomment to have KataGo apply small biases to the root search results
+# This can help reduce the "flat" evaluation of equal positions, but the bias applied is small.
+# Per-channel parameters that control the impact on the root node evaluation of various aspects:
+# Board edge proximity, areas next to already-played stones, corner proximity, etc.
+# positionalityTuneX1 = 0.2
+# positionalityTuneX2 = 0.0
+# positionalityTuneY1 = 1.0
+# positionalityTuneY2 = 0.15
+# positionalityTuneCenterDistance = 0.4
+# positionalityTuneOwnStoneProximity = 0.25
+# positionalityTuneOppStoneProximity = 0.0
\ No newline at end of file
diff --git a/shape/katago/downloader.py b/shape/katago/downloader.py
new file mode 100644
index 0000000..4db118d
--- /dev/null
+++ b/shape/katago/downloader.py
@@ -0,0 +1,410 @@
+import asyncio
+import os
+import platform
+import shutil
+import subprocess
+import zipfile
+from dataclasses import dataclass, field
+from pathlib import Path
+
+import httpx
+from PySide6.QtCore import QThread, Signal
+from PySide6.QtGui import QFont
+from PySide6.QtWidgets import (
+ QDialog,
+ QHBoxLayout,
+ QLabel,
+ QProgressBar,
+ QPushButton,
+ QVBoxLayout,
+ QWidget,
+)
+
+from shape.utils import setup_logging
+
+logger = setup_logging()
+
+KATRAIN_DIR = Path.home() / ".katrain"
+
+
+def get_katago_version_info(katago_path: Path) -> tuple[str, str]:
+ """Get KataGo version and backend info. Returns (version, backend)."""
+ try:
+ result = subprocess.run([str(katago_path), "version"], capture_output=True, text=True, timeout=10)
+ if result.returncode == 0:
+ lines = result.stdout.strip().split("\n")
+ version = "Unknown"
+ backend = "Unknown"
+
+ for line in lines:
+ if line.startswith("KataGo v"):
+ version = line.split()[1] # Extract version like "v1.15.3"
+ elif "backend" in line.lower():
+ # Extract backend like "OpenCL", "CUDA", etc.
+ if "OpenCL" in line:
+ backend = "OpenCL"
+ elif "CUDA" in line:
+ backend = "CUDA"
+ elif "CPU" in line:
+ backend = "CPU"
+ else:
+ backend = line.split()[-1] # Last word of the line
+
+ return version, backend
+ else:
+ logger.warning(f"KataGo version command failed: {result.stderr}")
+ return "Unknown", "Unknown"
+ except Exception as e:
+ logger.warning(f"Failed to get KataGo version: {e}")
+ return "Unknown", "Unknown"
+
+
+KATAGO_DIR = KATRAIN_DIR / "katago"
+KATAGO_EXE_NAME = "katago.exe" if platform.system() == "Windows" else "katago"
+KATAGO_PATH = KATAGO_DIR / KATAGO_EXE_NAME
+
+
+@dataclass
+class DownloadableComponent:
+ name: str
+ destination_dir: Path
+ destination_filename: str
+ download_url: str
+ is_zip: bool = False
+ found: bool = field(init=False, default=False)
+ downloading: bool = field(init=False, default=False)
+ error: str | None = field(init=False, default=None)
+
+ def __post_init__(self):
+ self.destination_dir.mkdir(parents=True, exist_ok=True)
+ self.check_if_found()
+
+ @property
+ def destination_path(self) -> Path:
+ return self.destination_dir / self.destination_filename
+
+ def check_if_found(self):
+ # Special case for KataGo: check PATH first
+ if self.name == "KataGo Engine":
+ path_katago = shutil.which("katago")
+ if path_katago:
+ self.found = True
+ self.error = None
+ # Update destination to point to the PATH version
+ self._path_katago = Path(path_katago)
+ return True
+
+ self.found = self.destination_path.exists()
+ if self.found:
+ self.error = None # reset error on found
+ return self.found
+
+ def get_widget(self, download_callback, parent_dialog) -> "ComponentWidget":
+ return ComponentWidget(self, download_callback, parent_dialog)
+
+
+class ComponentWidget(QWidget):
+ def __init__(self, component: DownloadableComponent, download_callback, parent: QDialog):
+ super().__init__(parent)
+ self.component = component
+ self.download_callback = download_callback
+
+ layout = QHBoxLayout()
+ self.name_label = QLabel(f"{component.name}")
+ self.status_label = QLabel()
+ self.download_button = QPushButton()
+ self.download_button.clicked.connect(self._on_download_click)
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setVisible(False)
+
+ layout.addWidget(self.name_label)
+ layout.addWidget(self.status_label)
+ layout.addStretch()
+ layout.addWidget(self.progress_bar)
+ layout.addWidget(self.download_button)
+ self.setLayout(layout)
+ self.update_status()
+
+ def _on_download_click(self):
+ self.download_button.setEnabled(False)
+ self.progress_bar.setVisible(True)
+ self.progress_bar.setRange(0, 0)
+ self.download_callback(self.component)
+
+ def update_status(self):
+ self.component.check_if_found()
+ if self.component.downloading:
+ self.status_label.setText("Downloading...")
+ self.download_button.setVisible(False)
+ self.progress_bar.setVisible(True)
+ elif self.component.found:
+ if self.component.name == "KataGo Engine":
+ # Show version info for KataGo
+ if hasattr(self.component, "_path_katago"):
+ katago_path = self.component._path_katago
+ location_text = f"Found in PATH: {katago_path}"
+ else:
+ katago_path = self.component.destination_path
+ location_text = f"Found at {katago_path}"
+
+ version, backend = get_katago_version_info(katago_path)
+ self.status_label.setText(
+ f"{location_text}
Version: {version} ({backend})"
+ )
+ else:
+ # For models, show file info
+ if hasattr(self.component, "_path_katago"):
+ self.status_label.setText(
+ f"Found in PATH: {self.component._path_katago}"
+ )
+ else:
+ file_size = (
+ self.component.destination_path.stat().st_size // (1024 * 1024)
+ if self.component.destination_path.exists()
+ else 0
+ )
+ self.status_label.setText(
+ f"Found at {self.component.destination_path}
Size: {file_size} MB"
+ )
+ self.download_button.setVisible(False)
+ self.progress_bar.setVisible(False)
+ elif self.component.error:
+ self.status_label.setText(f"Error: {self.component.error}")
+ self.download_button.setText("Retry")
+ self.download_button.setVisible(True)
+ self.download_button.setEnabled(True)
+ self.progress_bar.setVisible(False)
+ else:
+ self.status_label.setText("Missing")
+ self.download_button.setText("Download")
+ self.download_button.setVisible(True)
+ self.download_button.setEnabled(True)
+ self.progress_bar.setVisible(False)
+
+ def update_progress(self, percent):
+ self.progress_bar.setRange(0, 100)
+ self.progress_bar.setValue(percent)
+
+
+class DownloadThread(QThread):
+ progress_signal = Signal(DownloadableComponent, int)
+ finished_signal = Signal(DownloadableComponent, str) # Component, error_message (empty string for success)
+
+ def __init__(self, components_to_download: list[DownloadableComponent]):
+ super().__init__()
+ self.components = components_to_download
+ for c in self.components:
+ c.downloading = True
+
+ def run(self):
+ try:
+ asyncio.run(self._download_files_async())
+ except Exception as e:
+ logger.error(f"Downloader thread failed: {e}")
+ for component in self.components: # fail all on thread error
+ if component.downloading:
+ self.finished_signal.emit(component, str(e))
+
+ async def _download_files_async(self):
+ async with httpx.AsyncClient(timeout=300.0) as client:
+ tasks = [self._download_file(client, c) for c in self.components]
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+
+ for component, result in zip(self.components, results, strict=False):
+ if isinstance(result, Exception):
+ self.finished_signal.emit(component, str(result))
+ else:
+ self.finished_signal.emit(component, "")
+
+ async def _download_file(self, client: httpx.AsyncClient, component: DownloadableComponent):
+ download_path = (
+ component.destination_path.with_suffix(".zip.download")
+ if component.is_zip
+ else component.destination_path.with_suffix(".download")
+ )
+ try:
+ async with client.stream("GET", component.download_url, follow_redirects=True) as response:
+ response.raise_for_status()
+ total_size = int(response.headers.get("content-length", 0))
+ with open(download_path, "wb") as f:
+ downloaded = 0
+ async for chunk in response.aiter_bytes(chunk_size=8192):
+ f.write(chunk)
+ downloaded += len(chunk)
+ if total_size > 0:
+ percent = min(100, (downloaded * 100) // total_size)
+ self.progress_signal.emit(component, percent)
+
+ if component.is_zip:
+ with zipfile.ZipFile(download_path, "r") as zip_ref:
+ # find executable in zip
+ exe_files = [
+ f for f in zip_ref.namelist() if f.endswith(KATAGO_EXE_NAME) and not f.startswith("__MACOSX")
+ ]
+ if not exe_files:
+ raise Exception(f"Katago executable not found in zip {download_path}")
+ internal_exe_path = exe_files[0]
+ with zip_ref.open(internal_exe_path) as source, open(component.destination_path, "wb") as target:
+ shutil.copyfileobj(source, target)
+ os.chmod(component.destination_path, 0o755) # make executable
+ else:
+ shutil.move(download_path, component.destination_path)
+
+ finally:
+ if download_path.exists():
+ download_path.unlink()
+
+
+class ComponentsDownloaderDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("SHAPE Setup: KataGo Components")
+ self.setModal(True)
+ self.setMinimumWidth(600)
+
+ self.components = self._define_components()
+ self.component_widgets: dict[str, ComponentWidget] = {}
+ self.setup_ui()
+ self.check_all_found()
+
+ def _get_katago_url(self):
+ system = platform.system()
+ base_url = "https://github.com/lightvector/KataGo/releases/download/v1.16.0/"
+ if system == "Linux":
+ return base_url + "katago-v1.16.0-opencl-linux-x64.zip"
+ elif system == "Windows":
+ return base_url + "katago-v1.16.0-opencl-windows-x64.zip"
+ elif system == "Darwin": # MacOS
+ return base_url + "katago-v1.16.0-opencl-macos-x64.zip"
+ raise RuntimeError(f"Unsupported OS for KataGo download: {system}")
+
+ def _define_components(self) -> list[DownloadableComponent]:
+ return [
+ DownloadableComponent(
+ name="KataGo Engine",
+ destination_dir=KATAGO_DIR,
+ destination_filename=KATAGO_EXE_NAME,
+ download_url=self._get_katago_url(),
+ is_zip=True,
+ ),
+ DownloadableComponent(
+ name="KataGo Model (28b)",
+ destination_dir=KATRAIN_DIR,
+ destination_filename="katago-28b.bin.gz",
+ download_url="https://media.katagotraining.org/uploaded/networks/models/kata1/kata1-b28c512nbt-s7709128960-d4462231357.bin.gz",
+ ),
+ DownloadableComponent(
+ name="KataGo Model (Human)",
+ destination_dir=KATRAIN_DIR,
+ destination_filename="katago-human.bin.gz",
+ download_url="https://github.com/lightvector/KataGo/releases/download/v1.15.0/b18c384nbt-humanv0.bin.gz",
+ ),
+ ]
+
+ def setup_ui(self):
+ layout = QVBoxLayout()
+ self.title_label = QLabel("Checking for required components...")
+ font = QFont()
+ font.setPointSize(16)
+ font.setBold(True)
+ self.title_label.setFont(font)
+ layout.addWidget(self.title_label)
+
+ for component in self.components:
+ widget = component.get_widget(self.download_one, self)
+ self.component_widgets[component.name] = widget
+ layout.addWidget(widget)
+
+ self.download_all_button = QPushButton("Download All Missing")
+ self.download_all_button.clicked.connect(self.download_all)
+ layout.addWidget(self.download_all_button)
+
+ self.close_button = QPushButton("Close")
+ self.close_button.clicked.connect(self.accept)
+ layout.addWidget(self.close_button)
+
+ self.setLayout(layout)
+
+ def download_one(self, component: DownloadableComponent):
+ self.download([component])
+
+ def download_all(self):
+ missing = [c for c in self.components if not c.found]
+ self.download(missing)
+
+ def download(self, components: list[DownloadableComponent]):
+ if not components:
+ return
+ self.download_all_button.setEnabled(False)
+ self.close_button.setEnabled(False)
+
+ self.download_thread = DownloadThread(components)
+ for c in components:
+ self.component_widgets[c.name].update_status()
+ self.download_thread.progress_signal.connect(self._on_progress)
+ self.download_thread.finished_signal.connect(self._on_finished)
+ self.download_thread.start()
+
+ def _on_progress(self, component: DownloadableComponent, percent: int):
+ self.component_widgets[component.name].update_progress(percent)
+
+ def _on_finished(self, component: DownloadableComponent, error: str):
+ component.downloading = False
+ component.error = error if error else None
+ component.check_if_found()
+ # Force progress bar to be hidden and reset
+ widget = self.component_widgets[component.name]
+ widget.progress_bar.setVisible(False)
+ widget.progress_bar.setRange(0, 100)
+ widget.progress_bar.setValue(0)
+ widget.update_status()
+ self.check_all_found()
+
+ def check_all_found(self):
+ all_found = all(c.check_if_found() for c in self.components)
+ downloading = any(c.downloading for c in self.components)
+
+ # Update title based on status
+ if downloading:
+ self.title_label.setText("Downloading components...")
+ elif all_found:
+ self.title_label.setText("All components ready!")
+ else:
+ missing_count = sum(1 for c in self.components if not c.found)
+ self.title_label.setText(f"Missing {missing_count} component{'s' if missing_count != 1 else ''}")
+
+ self.download_all_button.setEnabled(not all_found and not downloading)
+ self.close_button.setEnabled(all_found and not downloading)
+ self.close_button.setText("Continue" if all_found else "Close")
+ if all_found:
+ self.download_all_button.setVisible(False)
+
+ def get_paths(self) -> dict[str, Path] | None:
+ if not all(c.found for c in self.components):
+ return None
+
+ # Get KataGo path - either from PATH or downloaded location
+ katago_component = self.components[0] # KataGo Engine is first
+ if hasattr(katago_component, "_path_katago"):
+ katago_path = katago_component._path_katago
+ else:
+ katago_path = KATAGO_PATH
+
+ return {
+ "katago_path": katago_path,
+ "model_path": self.components[1].destination_path,
+ "human_model_path": self.components[2].destination_path,
+ }
+
+ def get_katago_version_info(self) -> tuple[str, str]:
+ """Get KataGo version and backend info for the main window title."""
+ katago_component = self.components[0] # KataGo Engine is first
+ if not katago_component.found:
+ return "Unknown", "Unknown"
+
+ if hasattr(katago_component, "_path_katago"):
+ katago_path = katago_component._path_katago
+ else:
+ katago_path = KATAGO_PATH
+
+ return get_katago_version_info(katago_path)
diff --git a/shape/katago/engine.py b/shape/katago/engine.py
index 8d48401..3e44757 100644
--- a/shape/katago/engine.py
+++ b/shape/katago/engine.py
@@ -7,7 +7,10 @@
import traceback
from collections.abc import Callable
+from PySide6.QtWidgets import QApplication, QDialog
+
from shape.game_logic import GameNode
+from shape.katago.downloader import ComponentsDownloaderDialog
from shape.utils import setup_logging
logger = setup_logging()
@@ -24,33 +27,45 @@ class KataGoEngine:
"stone_scoring": "stone_scoring",
}
- def __init__(self, katago_path, model_folder=None):
- if model_folder == None:
- base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- else:
- base_dir = os.path.abspath(model_folder)
-
- config_path = os.path.join(base_dir, "analysis.cfg")
- model_path = os.path.join(base_dir, "katago-28b.bin.gz")
- human_model_path = os.path.join(base_dir, "katago-human.bin.gz")
- if not os.path.exists(config_path) or not os.path.exists(model_path) or not os.path.exists(human_model_path):
- raise RuntimeError("Models not found. Run install.sh to download the models.")
+ def __init__(self, model_folder=None):
+ # analysis.cfg is now stored in the package
+ config_path = os.path.join(os.path.dirname(__file__), "analysis.cfg")
+ if not os.path.exists(config_path):
+ raise RuntimeError(f"Analysis config not found at {config_path}")
+
+ app = QApplication.instance()
+ if app is None:
+ app = QApplication([])
+
+ dialog = ComponentsDownloaderDialog()
+ paths = dialog.get_paths()
+ if not paths:
+ result = dialog.exec()
+ if result != QDialog.DialogCode.Accepted:
+ raise RuntimeError("KataGo components are required but download was cancelled or failed.")
+ paths = dialog.get_paths()
+ if not paths:
+ raise RuntimeError("Could not retrieve component paths even after download dialog.")
+
+ # Store version info for the main window title
+ self.katago_version, self.katago_backend = dialog.get_katago_version_info()
command = [
- os.path.abspath(katago_path),
+ os.path.abspath(paths["katago_path"]),
"analysis",
"-config",
config_path,
"-model",
- model_path,
+ str(paths["model_path"]),
"-human-model",
- human_model_path,
+ str(paths["human_model_path"]),
]
self.query_queue = queue.Queue()
self.response_callbacks = {}
self.process = self._start_process(command)
if self.process.poll() is not None:
- raise RuntimeError(f"KataGo process exited unexpectedly on startup: {self.process.stderr.read()}")
+ stderr_output = self.process.stderr.read() if self.process.stderr else "No stderr available"
+ raise RuntimeError(f"KataGo process exited unexpectedly on startup: {stderr_output}")
threads = [
threading.Thread(target=self._log_stderr, daemon=True),
@@ -108,7 +123,7 @@ def analyze_position(self, node: GameNode, callback: Callable, human_profile_set
nodes = node.nodes_from_root
moves = [m for node in nodes for m in node.moves]
self.query_counter += 1
- query_id = f"{len(nodes)}_{(moves or ['root'])[-1]}_{human_profile_settings.get('humanSLProfile','ai')}_{max_visits}v_{self.query_counter}"
+ query_id = f"{len(nodes)}_{(moves or ['root'])[-1]}_{human_profile_settings.get('humanSLProfile', 'ai')}_{max_visits}v_{self.query_counter}"
query = {
"id": query_id,
"rules": self.RULESETS_ABBR.get(node.ruleset.lower(), node.ruleset.lower()),
@@ -157,5 +172,5 @@ def _log_response(self, response):
{k: v for k, v in move.items() if k in ["move", "visits", "winrate"]}
for move in response.get("moveInfos", [])
]
- response["moveInfos"] = moves[:5] + [f"{len(moves)-5} more..."] if len(moves) > 5 else moves
+ response["moveInfos"] = moves[:5] + [f"{len(moves) - 5} more..."] if len(moves) > 5 else moves
logger.debug(f"Received response: {json.dumps(response, indent=2)}")
diff --git a/shape/main.py b/shape/main.py
index 9fb99a9..7012e74 100644
--- a/shape/main.py
+++ b/shape/main.py
@@ -1,9 +1,6 @@
-import os
+import argparse
import signal
-import subprocess
import sys
-import traceback
-import argparse
from PySide6.QtWidgets import QApplication
@@ -17,20 +14,12 @@
class SHAPEApp:
- def __init__(self, katago_path=None, model_folder=None):
+ def __init__(self):
self.app = QApplication(sys.argv)
self.main_window = MainWindow()
- # Use 'which katago' to find the KataGo executable
- if katago_path == None:
- try:
- katago_path = subprocess.check_output(["which", "katago"]).decode().strip()
- except subprocess.CalledProcessError:
- self.show_error("KataGo not found in PATH. Please install KataGo and make sure it's accessible.")
- sys.exit(1)
-
try:
- self.katago = KataGoEngine(katago_path, model_folder)
+ self.katago = KataGoEngine()
except Exception as e:
self.show_error(f"Failed to initialize KataGo engine: {e}")
sys.exit(1)
@@ -46,15 +35,10 @@ def show_error(self, message):
def main():
- parser = argparse.ArgumentParser(
- description='SHAPE: Shape Habits Analysis and Personalized Evaluation')
- parser.add_argument('--katago', type=str,
- help='Path to the katago executable (optional)', default=None)
- parser.add_argument('--model_folder', type=str,
- help='Path to the model folder (optional)', default=None)
- args = parser.parse_args()
-
- shape = SHAPEApp(args.katago, args.model_folder)
+ parser = argparse.ArgumentParser(description="SHAPE: Shape Habits Analysis and Personalized Evaluation")
+ parser.parse_args()
+
+ shape = SHAPEApp()
sys.exit(shape.run())
diff --git a/shape/ui/board_view.py b/shape/ui/board_view.py
index ba995f5..863f924 100644
--- a/shape/ui/board_view.py
+++ b/shape/ui/board_view.py
@@ -1,17 +1,16 @@
import numpy as np
-from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
+from PySide6.QtCore import QPointF, QRectF, QSize, Qt
from PySide6.QtGui import (
QBrush,
QColor,
QFont,
- QLinearGradient,
QPainter,
QPen,
QRadialGradient,
)
from PySide6.QtWidgets import QSizePolicy, QWidget
-from shape.game_logic import GameNode, Move, PolicyData
+from shape.game_logic import Move, PolicyData
from shape.utils import setup_logging
logger = setup_logging()
@@ -34,7 +33,7 @@ class BoardView(QWidget):
def sizeHint(self):
return QSize(600, 600)
- def __init__(self, main_window: "MainWindow", parent=None):
+ def __init__(self, main_window, parent=None):
super().__init__(parent)
self.main_window = main_window
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -162,12 +161,12 @@ def draw_coordinates_and_nav(self, painter):
Qt.AlignVCenter | Qt.AlignRight,
str(i + 1),
)
- for (text, _, size_adj), nav_rect in zip(self.nav_buttons, self.nav_rects):
+ for (text, _, size_adj), nav_rect in zip(self.nav_buttons, self.nav_rects, strict=False):
painter.setFont(QFont("Arial", self.coord_font_size * size_adj, QFont.Bold))
painter.drawText(nav_rect, Qt.AlignCenter, text)
def mousePressEvent(self, event):
- for (_, callback, _), rect in zip(self.nav_buttons, self.nav_rects):
+ for (_, callback, _), rect in zip(self.nav_buttons, self.nav_rects, strict=False):
if rect.contains(event.pos()):
callback()
return
@@ -218,7 +217,7 @@ def draw_heatmap(self, painter, policy, sampling_settings):
rank_gradient = np.tile(np.linspace(0, 2, self.board_size), (self.board_size, 1)).T
heatmap_mean_prob = np.append(prob_gradient.ravel(), 0)
heatmap_mean_rank = np.append(rank_gradient.ravel(), 0)
- sampling_settings = dict(min_p=0)
+ sampling_settings = {"min_p": 0}
if heatmap_mean_prob is not None:
top_moves, _ = PolicyData(heatmap_mean_prob).sample(
@@ -236,7 +235,7 @@ def draw_heatmap_points(self, painter, top_moves, show_text=True):
x = center.x() - size / 2
y = center.y() - size / 2
- text = "" if rel_prob < 0.01 else f"{prob*100:.0f}"
+ text = "" if rel_prob < 0.01 else f"{prob * 100:.0f}"
painter.setBrush(QBrush(color))
painter.setPen(QPen(Qt.black))
diff --git a/shape/ui/main_window.py b/shape/ui/main_window.py
index f70bec3..1736e67 100644
--- a/shape/ui/main_window.py
+++ b/shape/ui/main_window.py
@@ -2,15 +2,15 @@
import numpy as np
from PySide6.QtCore import QEvent, Qt, QTimer, Signal
-from PySide6.QtGui import QAction, QKeySequence
+from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
QApplication,
+ QFileDialog,
QHBoxLayout,
QLabel,
QMainWindow,
QMenu,
QMenuBar,
- QPushButton,
QStatusBar,
QTabWidget,
QVBoxLayout,
@@ -47,6 +47,16 @@ def __init__(self):
def set_engine(self, katago_engine):
self.katago_engine = katago_engine
+ # Update window title with version info
+ try:
+ from importlib.metadata import version
+
+ shape_version = version("goshape")
+ except ImportError:
+ shape_version = "dev" # Fallback for development
+ katago_version = getattr(katago_engine, "katago_version", "Unknown")
+ katago_backend = getattr(katago_engine, "katago_backend", "Unknown")
+ self.setWindowTitle(f"SHAPE v{shape_version} running KataGo {katago_version} ({katago_backend})")
self.update_state()
def set_logging_level(self, level):
@@ -93,10 +103,10 @@ def maybe_make_ai_move(self, current_node, human_profiles, current_analysis, nex
best_ai_move = current_analysis[None].ai_moves()[0]["move"]
if policy_moves:
if best_ai_move == "pass":
- logger.info(f"Passing because it is the best AI move")
+ logger.info("Passing because it is the best AI move")
self.make_move(None)
else:
- moves, probs, _ = zip(*policy_moves)
+ moves, probs, _ = zip(*policy_moves, strict=False)
move = np.random.choice(moves, p=np.array(probs) / sum(probs))
logger.info(f"Making sampled move: {move} from {len(policy_moves)} cuttoff due to {reason}")
self.make_move(move.coords)
@@ -133,10 +143,10 @@ def copy_sgf_to_clipboard(self):
def save_as_sgf(self, to_clipboard: bool = False):
def get_player_name(color):
if self.control_panel.get_player_color() == color:
- return f"Human"
+ return "Human"
else:
profile = self.control_panel.get_human_profiles()["opponent"]
- return f"AI ({profile})" if profile else f"KataGo"
+ return f"AI ({profile})" if profile else "KataGo"
player_names = {bw: get_player_name(bw) for bw in "BW"}
sgf_data = self.game_logic.export_sgf(player_names)
@@ -218,7 +228,7 @@ def on_analysis_complete(self, node, analysis, human_profile):
node.store_analysis(analysis, human_profile)
num_queries = self.katago_engine.num_outstanding_queries()
self.update_status_bar(
- f"Ready"
+ "Ready"
if num_queries == 0
else f"{human_profile or 'AI'} analysis for {node.move.gtp() if node.move else 'root'} received, still working on {num_queries} queries"
)
@@ -321,7 +331,7 @@ def create_menu_bar(self):
menu_bar.addMenu(logging_menu)
for level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
logging_action = QAction(level.capitalize(), self)
- logging_action.triggered.connect(lambda l=level: self.set_logging_level(l))
+ logging_action.triggered.connect(lambda level=level: self.set_logging_level(level))
logging_menu.addAction(logging_action)
def on_pass_move(self):
diff --git a/shape/ui/tab_analysis.py b/shape/ui/tab_analysis.py
index d0abc88..7cedfa4 100644
--- a/shape/ui/tab_analysis.py
+++ b/shape/ui/tab_analysis.py
@@ -75,7 +75,7 @@ def update_ui(self):
self.top_moves_table.setRowCount(len(top_moves))
for row, move in enumerate(top_moves):
self.top_moves_table.setItem(row, 0, QTableWidgetItem(move["move"]))
- self.top_moves_table.setItem(row, 1, QTableWidgetItem(f"{move['winrate']*100:.1f}%"))
+ self.top_moves_table.setItem(row, 1, QTableWidgetItem(f"{move['winrate'] * 100:.1f}%"))
self.top_moves_table.setItem(row, 2, QTableWidgetItem(f"{move['scoreLead']:.1f}"))
self.top_moves_table.setItem(row, 3, QTableWidgetItem(f"{move['visits']}"))
@@ -95,7 +95,7 @@ def clear(self):
self.top_moves_table.clearContents()
def update_graph(self, scores: list[tuple[int, float]]):
- moves, filtered_values = zip(*scores)
+ moves, filtered_values = zip(*scores, strict=False)
self.graph_widget.plot(moves, filtered_values, pen=pg.mkPen(color="b", width=2), clear=True)
self.graph_widget.setYRange(min(filtered_values) - 0.1, max(filtered_values) + 0.1)
self.graph_widget.setXRange(0, max(1, len(moves) - 1))
diff --git a/shape/ui/tab_main_control.py b/shape/ui/tab_main_control.py
index b341143..bf477e4 100644
--- a/shape/ui/tab_main_control.py
+++ b/shape/ui/tab_main_control.py
@@ -27,14 +27,14 @@
def get_rank_from_id(id: int) -> str:
if id < 0:
return f"{-id}k"
- return f"{id+1}d"
+ return f"{id + 1}d"
def get_human_profile_from_id(id: int, preaz: bool = False) -> str | None:
if id >= RANK_RANGE[1] + 10:
return None # AI
if id >= RANK_RANGE[1]:
- return f"proyear_2023"
+ return "proyear_2023"
return f"{'preaz_' if preaz else 'rank_'}{get_rank_from_id(id)}"
@@ -237,16 +237,16 @@ def get_move_stats(self, node):
player_prob, player_relative_prob = currentlv_analysis.human_policy.at(node.move)
target_prob, target_relative_prob = target_analysis.human_policy.at(node.move)
ai_prob, ai_relative_prob = ai_analysis.ai_policy.at(node.move)
- return dict(
- player_prob=player_prob,
- target_prob=target_prob,
- ai_prob=ai_prob,
- player_relative_prob=player_relative_prob,
- target_relative_prob=target_relative_prob,
- ai_relative_prob=ai_relative_prob,
- move_like_target=target_prob / max(player_prob + target_prob, 1e-10),
- mistake_size=node.mistake_size(),
- )
+ return {
+ "player_prob": player_prob,
+ "target_prob": target_prob,
+ "ai_prob": ai_prob,
+ "player_relative_prob": player_relative_prob,
+ "target_relative_prob": target_relative_prob,
+ "ai_relative_prob": ai_relative_prob,
+ "move_like_target": target_prob / max(player_prob + target_prob, 1e-10),
+ "mistake_size": node.mistake_size(),
+ }
return None
def get_heatmap_settings(self):
diff --git a/shape/ui/ui_utils.py b/shape/ui/ui_utils.py
index 2ecdc22..7e07f6c 100644
--- a/shape/ui/ui_utils.py
+++ b/shape/ui/ui_utils.py
@@ -55,6 +55,7 @@
}
"""
+
# Helper functions
def create_spin_box(min_value, max_value, default_value):
spin_box = QSpinBox()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_game_logic.py b/tests/test_game_logic.py
new file mode 100644
index 0000000..2fdf117
--- /dev/null
+++ b/tests/test_game_logic.py
@@ -0,0 +1,42 @@
+import pytest
+
+from shape.game_logic import GameLogic, PolicyData
+
+
+def test_game_logic_initialization():
+ """Test that GameLogic can be initialized."""
+ game = GameLogic()
+ assert game is not None
+
+
+def test_game_logic_new_game():
+ """Test that new_game works with different board sizes."""
+ game = GameLogic()
+ game.new_game(board_size=19)
+ assert game.current_node.square_board_size == 19
+
+ game.new_game(board_size=13)
+ assert game.current_node.square_board_size == 13
+
+
+@pytest.mark.parametrize("board_size", [9, 13, 19])
+def test_valid_board_sizes(board_size):
+ """Test that valid board sizes work correctly."""
+ game = GameLogic()
+ game.new_game(board_size=board_size)
+ assert game.current_node.square_board_size == board_size
+
+
+def test_policy_data_initialization():
+ """Test PolicyData initialization."""
+ policy_data = [0.1] * 361 + [0.05] # 19x19 board + pass
+ policy = PolicyData(policy_data)
+ assert policy.pass_prob == 0.05
+ assert policy.grid.shape == (19, 19)
+
+
+def test_policy_data_grid_from_data():
+ """Test PolicyData grid creation."""
+ policy_data = [0.1] * 81 + [0.05] # 9x9 board + pass
+ grid = PolicyData.grid_from_data(policy_data)
+ assert grid.shape == (9, 9)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..fcb826b
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,6 @@
+from shape import utils
+
+
+def test_utils_module_exists():
+ """Test that utils module can be imported."""
+ assert utils is not None
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..dddf34b
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,487 @@
+version = 1
+revision = 2
+requires-python = ">=3.10, <3.13"
+resolution-markers = [
+ "python_full_version >= '3.11'",
+ "python_full_version < '3.11'",
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.6.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" },
+ { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" },
+ { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" },
+ { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" },
+ { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" },
+ { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" },
+ { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" },
+ { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" },
+ { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" },
+ { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" },
+ { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" },
+ { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" },
+ { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" },
+ { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" },
+ { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "goshape"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "httpx" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pyqtgraph" },
+ { name = "pysgf" },
+ { name = "pyside6" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "httpx", specifier = ">=0.25.0" },
+ { name = "numpy", specifier = ">=2.1.2" },
+ { name = "pyqtgraph", specifier = ">=0.13.7" },
+ { name = "pysgf", specifier = ">=0.9.0" },
+ { name = "pyside6", specifier = ">=6.5.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "pytest", specifier = ">=7.0.0" },
+ { name = "pytest-cov", specifier = ">=4.0.0" },
+ { name = "ruff", specifier = ">=0.1.0" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
+ { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
+ { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
+ { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
+ { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
+ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346, upload-time = "2025-06-21T11:47:47.57Z" },
+ { url = "https://files.pythonhosted.org/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143, upload-time = "2025-06-21T11:48:10.766Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989, upload-time = "2025-06-21T11:48:19.998Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890, upload-time = "2025-06-21T11:48:31.376Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032, upload-time = "2025-06-21T11:48:52.563Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354, upload-time = "2025-06-21T11:49:17.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605, upload-time = "2025-06-21T11:49:41.161Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994, upload-time = "2025-06-21T11:50:08.516Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672, upload-time = "2025-06-21T11:50:19.584Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015, upload-time = "2025-06-21T11:50:39.139Z" },
+ { url = "https://files.pythonhosted.org/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989, upload-time = "2025-06-21T11:50:55.616Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" },
+ { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" },
+ { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" },
+ { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637, upload-time = "2025-06-21T12:26:12.518Z" },
+ { url = "https://files.pythonhosted.org/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087, upload-time = "2025-06-21T12:26:22.294Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588, upload-time = "2025-06-21T12:26:32.939Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010, upload-time = "2025-06-21T12:26:54.086Z" },
+ { url = "https://files.pythonhosted.org/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042, upload-time = "2025-06-21T12:27:19.018Z" },
+ { url = "https://files.pythonhosted.org/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246, upload-time = "2025-06-21T12:27:38.618Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyqtgraph"
+version = "0.13.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/d9/b62d5cddb3caa6e5145664bee5ed90223dee23ca887ed3ee479f2609e40a/pyqtgraph-0.13.7.tar.gz", hash = "sha256:64f84f1935c6996d0e09b1ee66fe478a7771e3ca6f3aaa05f00f6e068321d9e3", size = 2343380, upload-time = "2024-04-29T02:18:58.467Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/34/5702b3b7cafe99be1d94b42f100e8cc5e6957b761fcb1cf5f72d492851da/pyqtgraph-0.13.7-py3-none-any.whl", hash = "sha256:7754edbefb6c367fa0dfb176e2d0610da3ada20aa7a5318516c74af5fb72bf7a", size = 1925473, upload-time = "2024-04-29T02:18:56.206Z" },
+]
+
+[[package]]
+name = "pysgf"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/f9/69e89a008d39214ba15b832a0a8732d41e46200174fd081497284de48d02/pysgf-0.9.0.tar.gz", hash = "sha256:ff6a95e3891818a12e09af022aebec0bfaeca8617599f34e647a94ae02ee0303", size = 4960, upload-time = "2024-06-01T10:11:48.96Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/ad/5398ec34cfe583b683604ad9559c403581184552978463c87b5a901454ef/pysgf-0.9.0-py3-none-any.whl", hash = "sha256:b72fb7d5ba730a6a49d456feb412d15082e0fa834cbccbed0c2f8d88a6f07f89", size = 5376, upload-time = "2024-06-01T10:11:47.61Z" },
+]
+
+[[package]]
+name = "pyside6"
+version = "6.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyside6-addons" },
+ { name = "pyside6-essentials" },
+ { name = "shiboken6" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/91/8e9c7f7e90431297de9856e90a156ade9420977e26d87996909c63f30bd2/PySide6-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:f843ef39970a2f79757810fffd7b8e93ac42a3de9ea62f2a03648cde57648aed", size = 558097, upload-time = "2025-06-03T13:20:03.739Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ff/04d1b6b30edd24d761cc30d964860f997bdf37d06620694bf9aab35eec3a/PySide6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:db44ac08b8f7ac1b421bc1c6a44200d03f08d80dc7b3f68dfdb1684f30f41c17", size = 558239, upload-time = "2025-06-03T13:20:06.205Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b4/ca076c55c11a8e473363e05aa82c5c03dd7ba8f17b77cc9311ce17213193/PySide6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:531a6e67c429b045674d57fe9864b711eb59e4cded753c2640982e368fd468d1", size = 558239, upload-time = "2025-06-03T13:20:08.257Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ff/95c941f53b0faebc27dbe361d8e971b77f504b9cf36f8f5d750fd82cd6fc/PySide6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:c82dbb7d32bbdd465e01059174f71bddc97de152ab71bded3f1907c40f9a5f16", size = 564571, upload-time = "2025-06-03T13:20:10.321Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/ef/0aa5e910fa4e9770db6b45c23e360a52313922e0ca71fc060a57db613de1/PySide6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:1525d63dc6dc425b8c2dc5bc01a8cb1d67530401449f3a3490c09a14c095b9f9", size = 401793, upload-time = "2025-06-03T13:20:12.108Z" },
+]
+
+[[package]]
+name = "pyside6-addons"
+version = "6.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyside6-essentials" },
+ { name = "shiboken6" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/e2/39b9e04335d7ac782b6459bf7abec90c36b8efaac5a88ef818e972c59387/PySide6_Addons-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:7be0708fa89715c282541fca47e2ba97c0c8d2886e0236ef994b2dd8f52aacdd", size = 316212438, upload-time = "2025-06-03T13:06:15.027Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/6f/691d7039a6f7943522a770b713ecd85fa169688dfdd65ddd4db1699d01b6/PySide6_Addons-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:da7869b02e3599d26546fad582db4656060786bc5ec8ece5ec9ee8aa8b42371c", size = 166690468, upload-time = "2025-06-03T13:06:34.962Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/08/a264db09ad35819643d910cd4c73a86f72f23b7092f8ebc7e51dcca53a86/PySide6_Addons-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:53fd08c8152b6ba8c435458afd189835ba905793a5077a2bb0b1b11222b375d4", size = 162466096, upload-time = "2025-06-03T13:08:58.065Z" },
+ { url = "https://files.pythonhosted.org/packages/84/be/a849402f7e73d137b5ae8b4370a49b0cf0e0c02f028b845782cb743e4995/PySide6_Addons-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:cd93a3a5e3886cd958f3a5acc7c061c24f10a394ce9f4ce657ac394544ca7ec2", size = 143150906, upload-time = "2025-06-03T13:09:12.762Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/f1/1bb6b5859aff4e2b3f5ef789b9cee200811a9f469f04d9aa7425e816622b/PySide6_Addons-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:4f589631bdceb518080ae9c9fa288e64f092cd5bebe25adc8ad89e8eadd4db29", size = 26938762, upload-time = "2025-06-03T13:09:20.009Z" },
+]
+
+[[package]]
+name = "pyside6-essentials"
+version = "6.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "shiboken6" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/59/714874db9ef3bbbbda654fd3223248969bea02ec1a5bfdd1c941c4e97749/PySide6_Essentials-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ed43435a70e018e1c22efcaf34a9430b83cfcad716dba661b03de21c13322fab", size = 132957077, upload-time = "2025-06-03T13:11:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/59/6a/ea0db68d40a1c487fd255634896f4e37b6560e3ef1f57ca5139bf6509b1f/PySide6_Essentials-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e5da48883f006c6206ef85874db74ddebcdf69b0281bd4f1642b1c5ac1d54aea", size = 96416183, upload-time = "2025-06-03T13:12:48.945Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/2f/4243630d1733522638c4967d36018c38719d8b84f5246bf3d4c010e0aa9d/PySide6_Essentials-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:e46a2801c9c6098025515fd0af6c594b9e9c951842f68b8f6f3da9858b9b26c2", size = 94171343, upload-time = "2025-06-03T13:12:59.426Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/a9/a8e0209ba9116f2c2db990cfb79f2edbd5a3a428013be2df1f1cddd660a9/PySide6_Essentials-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:ad1ac94011492dba33051bc33db1c76a7d6f815a81c01422cb6220273b369145", size = 72435676, upload-time = "2025-06-03T13:13:08.805Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/e4/23268c57e775a1a4d2843d288a9583a47f2e4b3977a9ae93cb9ded1a4ea5/PySide6_Essentials-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:35c2c2bb4a88db74d11e638cf917524ff35785883f10b439ead07960a5733aa4", size = 49483707, upload-time = "2025-06-03T13:13:16.399Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426, upload-time = "2025-06-26T20:34:14.784Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649, upload-time = "2025-06-26T20:33:39.242Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201, upload-time = "2025-06-26T20:33:42.207Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769, upload-time = "2025-06-26T20:33:44.102Z" },
+ { url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902, upload-time = "2025-06-26T20:33:45.98Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002, upload-time = "2025-06-26T20:33:47.81Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522, upload-time = "2025-06-26T20:33:49.857Z" },
+ { url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264, upload-time = "2025-06-26T20:33:52.199Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882, upload-time = "2025-06-26T20:33:54.231Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941, upload-time = "2025-06-26T20:33:56.202Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887, upload-time = "2025-06-26T20:33:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742, upload-time = "2025-06-26T20:34:00.465Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909, upload-time = "2025-06-26T20:34:02.603Z" },
+ { url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005, upload-time = "2025-06-26T20:34:04.723Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579, upload-time = "2025-06-26T20:34:06.766Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495, upload-time = "2025-06-26T20:34:08.718Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485, upload-time = "2025-06-26T20:34:11.008Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209, upload-time = "2025-06-26T20:34:12.928Z" },
+]
+
+[[package]]
+name = "shiboken6"
+version = "6.9.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/98/34d4d25b79055959b171420d47fcc10121aefcbb261c91d5491252830e31/shiboken6-6.9.1-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:40e92afc88da06b5100c56b761e59837ff282166e9531268f3d910b6128e621e", size = 406159, upload-time = "2025-06-03T13:16:45.104Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/07/53b2532ecd42ff925feb06b7bb16917f5f99f9c3470f0815c256789d818b/shiboken6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efcdfa8655d34aaf8d7a0c7724def3440bd46db02f5ad3b1785db5f6ccb0a8ff", size = 206756, upload-time = "2025-06-03T13:16:46.528Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b0/75b86ee3f7b044e6a87fbe7abefd1948ca4ae5fcde8321f4986a1d9eaa5e/shiboken6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:efcf75d48a29ae072d0bf54b3cd5a59ae91bb6b3ab7459e17c769355486c2e0b", size = 203233, upload-time = "2025-06-03T13:16:48.264Z" },
+ { url = "https://files.pythonhosted.org/packages/30/56/00af281275aab4c79e22e0ea65feede0a5c6da3b84e86b21a4a0071e0744/shiboken6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:209ccf02c135bd70321143dcbc5023ae0c056aa4850a845955dd2f9b2ff280a9", size = 1153587, upload-time = "2025-06-03T13:16:50.454Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ce/6ccd382fbe1a96926c5514afa6f2c42da3a9a8482e61f8dfc6068a9ca64f/shiboken6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:2a39997ce275ced7853defc89d3a1f19a11c90991ac6eef3435a69bb0b7ff1de", size = 1831623, upload-time = "2025-06-03T13:16:52.468Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
+]