GUI rework to Qt6
This commit is contained in:
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,10 +1,19 @@
|
||||
.venv
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
build
|
||||
.claude
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
build/
|
||||
.claude/
|
||||
.env
|
||||
|
||||
# Config a temp soubory
|
||||
*.!tag
|
||||
*.!ftag
|
||||
*.!gtag
|
||||
*.!gtag
|
||||
|
||||
# Documentation not to commit
|
||||
DESIGN_DOCUMENT.md
|
||||
AGENTS.md
|
||||
.claudeignore
|
||||
TEMPLATE.md
|
||||
175
CHANGELOG.md
175
CHANGELOG.md
@@ -1,68 +1,133 @@
|
||||
# Changelog
|
||||
|
||||
Všechny významné změny v projektu Tagger jsou dokumentovány v tomto souboru.
|
||||
All notable changes to the Tagger project are documented in this file.
|
||||
|
||||
## [1.1.0] - 2026-01-23
|
||||
|
||||
### Changed
|
||||
- **GUI rewrite to PySide6/Qt6** - Complete UI rewrite from Tkinter to Qt
|
||||
- Modern QMainWindow with menu bar, toolbar, and status bar
|
||||
- QTreeWidget for tag sidebar with category colors
|
||||
- QTableWidget for file list with sorting and filtering
|
||||
- QSplitter for resizable sidebar
|
||||
- Native Qt dialogs (QFileDialog, QInputDialog, QMessageBox)
|
||||
- Keyboard shortcuts using QShortcut
|
||||
- Window geometry persistence
|
||||
- **UI utilities updated** - `src/ui/utils.py` now uses Qt (QIcon, QPixmap)
|
||||
- **Python version restricted** - Requires Python >=3.13,<3.15 for PySide6 compatibility
|
||||
|
||||
### Dependencies
|
||||
- Added PySide6 (>=6.10.1)
|
||||
- Removed Tkinter dependency
|
||||
|
||||
## [1.0.5] - 2026-01-23
|
||||
|
||||
### Added
|
||||
- **Tag and category renaming** - New context menu functionality
|
||||
- Right-click on tag → "Rename tag"
|
||||
- Automatic update of all files with the tag
|
||||
- Support for renaming entire categories
|
||||
- **Tag merging** - When renaming to an existing tag
|
||||
- Confirmation dialog for merge
|
||||
- Merge removes source tag and updates files
|
||||
- **Tag.from_string()** - New class method for parsing tags
|
||||
- Parses "category/name" format
|
||||
- Eliminates duplicate code across the project
|
||||
- **Dynamic version loading** - Version is loaded from pyproject.toml
|
||||
- Fallback to `_version.py` if toml is not available
|
||||
- DEBUG mode support from `.env` (adds " DEV" suffix)
|
||||
- APP_NAME includes version: "Tagger v1.0.5 DEV"
|
||||
- **UI utilities module** - `src/ui/utils.py`
|
||||
- Moved `load_icon()` function from core to UI layer
|
||||
|
||||
### Changed
|
||||
- **FileManager refactoring**
|
||||
- New methods `assign_tag_to_files()` and `remove_tag_from_files()`
|
||||
- Old methods kept as deprecated aliases
|
||||
- `Tag` import at module level (eliminates duplicate imports)
|
||||
- **Dead code removal**
|
||||
- Deleted unused `ListManager` module
|
||||
- Removed legacy functions `load_config()` and `save_config()` from config.py
|
||||
- **Missing import fix** - Added `import subprocess` to media_utils.py
|
||||
|
||||
### Tests
|
||||
- 274 tests (all passing)
|
||||
- New tests for Tag.from_string() (6 tests)
|
||||
- New tests for rename/merge tags (24 tests)
|
||||
|
||||
## [1.0.4] - 2025-12-29
|
||||
|
||||
### Added
|
||||
- **CSFD.cz integration** - Fetching movie information
|
||||
- `fetch_movie()` - load movie details from URL
|
||||
- `search_movies()` - search for movies
|
||||
- Automatic tag assignment (genres, year, country, director)
|
||||
- **Close folder** - Safe folder closing with metadata saving
|
||||
|
||||
### Tests
|
||||
- 249 tests covering CSFD integration
|
||||
|
||||
## [1.0.3] - 2025-12-28
|
||||
|
||||
### Přidáno
|
||||
- **Hardlink struktura** - Nová funkcionalita pro vytváření adresářové struktury pomocí hardlinků
|
||||
- `HardlinkManager` třída v `src/core/hardlink_manager.py`
|
||||
- Vytváření hardlinků podle tagů souborů (např. `output/žánr/Komedie/film.mkv`)
|
||||
- Synchronizace struktury - detekce a odstranění zastaralých hardlinků při změně tagů
|
||||
- Podpora filtrování podle kategorií
|
||||
- Preview režim (dry run)
|
||||
- **Menu položky pro hardlinky**
|
||||
- "Nastavit hardlink složku..." - konfigurace výstupní složky a kategorií (ukládá se do `.tagger.json`)
|
||||
- "Aktualizovat hardlink strukturu" - rychlá synchronizace s uloženým nastavením
|
||||
- "Vytvořit hardlink strukturu..." - ruční výběr složky a kategorií
|
||||
- **Tříúrovňový konfigurační systém**
|
||||
- Globální config (`config.json`) - nastavení aplikace (geometrie okna, poslední složka)
|
||||
- Složkový config (`.tagger.json`) - nastavení projektu (ignore patterns, hardlink nastavení)
|
||||
- Souborové tagy (`.filename.!tag`) - metadata jednotlivých souborů
|
||||
- **Výchozí tagy**
|
||||
- Kategorie "Hodnocení" s hvězdičkami (1-5 hvězd)
|
||||
- Kategorie "Barva" s barevnými štítky
|
||||
- Exkluzivní výběr v kategorii Hodnocení (pouze jeden tag)
|
||||
- **Testy**
|
||||
- 189 testů pokrývajících všechny moduly
|
||||
- Testy pro hardlink manager včetně synchronizace
|
||||
- **Poetry** - Správa závislostí pomocí Poetry
|
||||
### Added
|
||||
- **Hardlink structure** - New functionality for creating directory structure using hardlinks
|
||||
- `HardlinkManager` class in `src/core/hardlink_manager.py`
|
||||
- Creating hardlinks based on file tags (e.g., `output/genre/Comedy/movie.mkv`)
|
||||
- Structure synchronization - detection and removal of outdated hardlinks when tags change
|
||||
- Support for filtering by categories
|
||||
- Preview mode (dry run)
|
||||
- **Menu items for hardlinks**
|
||||
- "Set hardlink folder..." - configure output folder and categories (saved to `.tagger.json`)
|
||||
- "Update hardlink structure" - quick sync with saved settings
|
||||
- "Create hardlink structure..." - manual folder and category selection
|
||||
- **Three-level configuration system**
|
||||
- Global config (`config.json`) - application settings (window geometry, last folder)
|
||||
- Folder config (`.tagger.json`) - project settings (ignore patterns, hardlink settings)
|
||||
- File tags (`.filename.!tag`) - individual file metadata
|
||||
- **Default tags**
|
||||
- "Rating" category with stars (1-5 stars)
|
||||
- "Color" category with color labels
|
||||
- Exclusive selection in Rating category (only one tag)
|
||||
- **Tests**
|
||||
- 189 tests covering all modules
|
||||
- Tests for hardlink manager including synchronization
|
||||
- **Poetry** - Dependency management using Poetry
|
||||
|
||||
### Změněno
|
||||
- Modernizované GUI inspirované qBittorrentem
|
||||
- Ukládání geometrie okna do globálního configu
|
||||
- Ignore patterns se ukládají do složkového configu
|
||||
### Changed
|
||||
- Modernized GUI inspired by qBittorrent
|
||||
- Window geometry saved to global config
|
||||
- Ignore patterns saved to folder config
|
||||
|
||||
## [1.0.2] - 2025-10-03
|
||||
|
||||
### Přidáno
|
||||
- **Moderní GUI** - Přepracované rozhraní ve stylu qBittorrent
|
||||
- Postranní panel s kategoriemi a tagy
|
||||
- Tabulka souborů s řazením podle sloupců
|
||||
- Kontextová menu pro soubory a tagy
|
||||
- Vyhledávací pole
|
||||
- Stavový řádek s počtem souborů a velikostí výběru
|
||||
- **Hromadné přiřazování tagů** - Dialog pro přiřazení tagů více souborům najednou
|
||||
- Třístav checkboxy (zaškrtnuto/nezaškrtnuto/smíšené)
|
||||
- Barevné rozlišení kategorií
|
||||
- **Detekce rozlišení videa** - Automatická detekce pomocí ffprobe
|
||||
- **Klávesové zkratky**
|
||||
- Ctrl+O - Otevřít složku
|
||||
- Ctrl+T - Přiřadit tagy
|
||||
- Ctrl+D - Nastavit datum
|
||||
- F5 - Obnovit
|
||||
- Delete - Odstranit z indexu
|
||||
### Added
|
||||
- **Modern GUI** - Redesigned interface in qBittorrent style
|
||||
- Side panel with categories and tags
|
||||
- File table with column sorting
|
||||
- Context menus for files and tags
|
||||
- Search field
|
||||
- Status bar with file count and selection size
|
||||
- **Bulk tag assignment** - Dialog for assigning tags to multiple files at once
|
||||
- Tri-state checkboxes (checked/unchecked/mixed)
|
||||
- Color-coded categories
|
||||
- **Video resolution detection** - Automatic detection using ffprobe
|
||||
- **Keyboard shortcuts**
|
||||
- Ctrl+O - Open folder
|
||||
- Ctrl+T - Assign tags
|
||||
- Ctrl+D - Set date
|
||||
- F5 - Refresh
|
||||
- Delete - Remove from index
|
||||
|
||||
### Změněno
|
||||
- Refaktorizace struktury projektu do modulů (`src/core/`, `src/ui/`)
|
||||
- Použití dataclass pro Tag a File objekty
|
||||
### Changed
|
||||
- Project structure refactored into modules (`src/core/`, `src/ui/`)
|
||||
- Using dataclass for Tag and File objects
|
||||
|
||||
## [1.0.0] - 2025-09-03
|
||||
|
||||
### Přidáno
|
||||
- Základní funkcionalita tagování souborů
|
||||
- Ukládání tagů do skrytých souborů (`.filename.!tag`)
|
||||
- Správa kategorií a tagů
|
||||
- Rekurzivní skenování složek
|
||||
- Ignore patterns pro filtrování souborů
|
||||
- Základní GUI v Tkinter
|
||||
### Added
|
||||
- Basic file tagging functionality
|
||||
- Storing tags in hidden files (`.filename.!tag`)
|
||||
- Category and tag management
|
||||
- Recursive folder scanning
|
||||
- Ignore patterns for file filtering
|
||||
- Basic GUI in Tkinter
|
||||
|
||||
101
PROJECT.md
Normal file
101
PROJECT.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Tagger - Project Documentation
|
||||
|
||||
**Version:** 1.1.0 | **Status:** Stable | **GUI:** PySide6/Qt6
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
Desktop app for organizing files using hierarchical tags (category/name).
|
||||
|
||||
**Features:** Folder scanning, tag filtering, rename/merge tags, CSFD.cz integration, hardlink structure, 3-level config (global/folder/file).
|
||||
|
||||
---
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Tagger/
|
||||
├── Tagger.py # Entry point
|
||||
├── src/core/ # Business logic (NO UI imports!)
|
||||
│ ├── tag.py # Tag value object (immutable)
|
||||
│ ├── tag_manager.py # Tag/category management
|
||||
│ ├── file.py # File with metadata
|
||||
│ ├── file_manager.py # File management, filtering
|
||||
│ ├── config.py # 3-level config system
|
||||
│ ├── hardlink_manager.py
|
||||
│ ├── csfd.py # CSFD scraper
|
||||
│ ├── constants.py # APP_NAME, VERSION
|
||||
│ └── _version.py # Version fallback for PyInstaller
|
||||
├── src/ui/
|
||||
│ ├── gui.py # Qt6 GUI (MainWindow)
|
||||
│ └── utils.py # load_icon()
|
||||
└── tests/ # 274 tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
1. **UI must not contain business logic** - call FileManager/TagManager
|
||||
2. **Core must not import UI** - no PySide6/tkinter in src/core/
|
||||
3. **Dependency injection** - pass via constructor
|
||||
4. **UTF-8 everywhere** - `encoding='utf-8'`, `ensure_ascii=False`
|
||||
|
||||
---
|
||||
|
||||
## Config Files
|
||||
|
||||
| Level | File | Contents |
|
||||
|-------|------|----------|
|
||||
| Global | `.Tagger.!gtag` | window geometry, last folder |
|
||||
| Folder | `.Tagger.!ftag` | ignore patterns, hardlink settings |
|
||||
| File | `.filename.!tag` | tags, date, state |
|
||||
|
||||
---
|
||||
|
||||
## Key Components
|
||||
|
||||
**Tag** - immutable, `Tag(category, name)`, `Tag.from_string("cat/name")`
|
||||
|
||||
**File** - `file_path`, `tags[]`, `date`, `csfd_url`, metadata in `.filename.!tag`
|
||||
|
||||
**TagManager** - `add_tag()`, `get_categories()`, `rename_tag()`, `merge_tag()`
|
||||
|
||||
**FileManager** - `append(folder)`, `filter_files_by_tags()`, `close_folder()`
|
||||
|
||||
**HardlinkManager** - `create_structure_for_files()`, `sync_structure()`
|
||||
|
||||
---
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
poetry run python Tagger.py # GUI
|
||||
poetry run pytest -q # Tests
|
||||
poetry run pyinstaller --onefile Tagger.py # Build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shortcuts
|
||||
|
||||
`Ctrl+O` Open | `Ctrl+T` Tags | `Ctrl+D` Date | `F5` Refresh | `Del` Remove
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
**Version 0.0.0 in build:** Run app once from source to update `_version.py`, then rebuild.
|
||||
|
||||
**Cannot import:** Use `poetry run python Tagger.py`
|
||||
|
||||
**Metadata corrupted:** Auto-recovers with defaults.
|
||||
|
||||
---
|
||||
|
||||
## Metrics
|
||||
|
||||
- **Tests:** 274 ✅
|
||||
- **Python:** 3.13+
|
||||
- **Dependencies:** PySide6, Pillow, requests, beautifulsoup4
|
||||
562
PROJECT_NOTES.md
562
PROJECT_NOTES.md
@@ -1,562 +0,0 @@
|
||||
# Tagger - Centrální Poznámky Projektu
|
||||
|
||||
> **DŮLEŽITÉ:** Tento soubor obsahuje VŠE co potřebuji vědět o projektu.
|
||||
> Pokud pracuji na Tagger, VŽDY nejdříve přečtu tento soubor!
|
||||
|
||||
**Poslední aktualizace:** 2025-12-29
|
||||
**Verze:** 1.0.4
|
||||
**Status:** Stable, v aktivním vývoji
|
||||
|
||||
---
|
||||
|
||||
## O projektu
|
||||
|
||||
**Tagger** je desktopová aplikace pro správu a organizaci souborů pomocí hierarchických tagů (štítků).
|
||||
|
||||
**Hlavní funkce:**
|
||||
- Rekurzivní procházení složek
|
||||
- Hierarchické tagy (kategorie/název)
|
||||
- Filtrování podle tagů
|
||||
- Přejmenování tagů a kategorií (včetně aktualizace všech souborů)
|
||||
- Metadata uložená v JSON souborech
|
||||
- Automatická detekce rozlišení videí (ffprobe)
|
||||
- Moderní GUI (qBittorrent-style)
|
||||
- Hardlink struktura - vytváření adresářové struktury pomocí hardlinků podle tagů
|
||||
- Tříúrovňový konfigurační systém (globální, složkový, souborový)
|
||||
- CSFD.cz integrace - získávání informací o filmech z české filmové databáze
|
||||
|
||||
---
|
||||
|
||||
## Struktura projektu
|
||||
|
||||
```
|
||||
Tagger/
|
||||
├── Tagger.py # Entry point
|
||||
├── PROJECT_NOTES.md # ← TENTO SOUBOR - HLAVNÍ ZDROJ PRAVDY
|
||||
├── CHANGELOG.md # Historie verzí
|
||||
├── pyproject.toml # Poetry konfigurace
|
||||
├── poetry.lock # Zamčené verze závislostí
|
||||
├── .gitignore # Git ignore pravidla
|
||||
│
|
||||
├── src/
|
||||
│ ├── core/ # Jádro aplikace (ŽÁDNÉ UI!)
|
||||
│ │ ├── tag.py # Tag value object (immutable, from_string parser)
|
||||
│ │ ├── tag_manager.py # Správa tagů a kategorií
|
||||
│ │ ├── file.py # File s metadaty
|
||||
│ │ ├── file_manager.py # Správa souborů, filtrování
|
||||
│ │ ├── config.py # Tříúrovňová konfigurace (global, folder, file)
|
||||
│ │ ├── hardlink_manager.py # Správa hardlink struktury
|
||||
│ │ ├── utils.py # list_files() - rekurzivní procházení
|
||||
│ │ ├── media_utils.py # add_video_resolution_tag (ffprobe)
|
||||
│ │ ├── csfd.py # CSFD.cz scraper (fetch_movie, search_movies)
|
||||
│ │ └── constants.py # APP_NAME, VERSION, APP_VIEWPORT
|
||||
│ │
|
||||
│ └── ui/
|
||||
│ ├── gui.py # Moderní qBittorrent-style GUI
|
||||
│ └── utils.py # load_icon() - GUI utility pro ikony
|
||||
│
|
||||
├── tests/ # 274 testů, 100% core coverage
|
||||
│ ├── __init__.py
|
||||
│ ├── conftest.py # Pytest fixtures
|
||||
│ ├── test_tag.py # 19 testů (včetně Tag.from_string)
|
||||
│ ├── test_tag_manager.py # 55 testů (včetně rename/merge tagů/kategorií)
|
||||
│ ├── test_file.py # 33 testů (včetně CSFD integrace)
|
||||
│ ├── test_file_manager.py # 78 testů (close_folder, rename/merge v souborech)
|
||||
│ ├── test_config.py # 31 testů
|
||||
│ ├── test_hardlink_manager.py # 28 testů
|
||||
│ ├── test_utils.py # 17 testů
|
||||
│ ├── test_media_utils.py # 3 testy (load_icon v src/ui/utils.py)
|
||||
│ └── test_csfd.py # 19 testů
|
||||
│
|
||||
├── src/resources/
|
||||
│ └── images/32/ # Ikony (32x32 PNG)
|
||||
│ ├── 32_unchecked.png
|
||||
│ ├── 32_checked.png
|
||||
│ └── 32_tag.png
|
||||
│
|
||||
└── data/samples/ # Testovací data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architektura
|
||||
|
||||
### Vrstvová struktura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Presentation (UI) │ ← gui.py
|
||||
│ - Tkinter GUI │ - NESMÍ obsahovat business logiku
|
||||
│ - Jen zobrazení + interakce │ - NESMÍ importovat přímo z core
|
||||
├─────────────────────────────────┤
|
||||
│ Business Logic │ ← FileManager, TagManager, HardlinkManager
|
||||
│ - Správa souborů/tagů │ - Callable z UI
|
||||
│ - Filtrování, validace │ - Callback pattern pro notifikace
|
||||
├─────────────────────────────────┤
|
||||
│ Data Layer │ ← File, Tag (models)
|
||||
│ - File, Tag třídy │ - Immutable kde je možné
|
||||
│ - Validation logic │ - __eq__ a __hash__ správně
|
||||
├─────────────────────────────────┤
|
||||
│ Persistence │ ← config.py, .!tag soubory
|
||||
│ - JSON soubory │ - UTF-8 encoding VŽDY
|
||||
│ - Config management │ - ensure_ascii=False
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Tříúrovňový konfigurační systém
|
||||
|
||||
1. **Globální config** (`.Tagger.!gtag` vedle Tagger.py)
|
||||
- Geometrie okna, maximalizace
|
||||
- Poslední otevřená složka
|
||||
- Recent folders
|
||||
|
||||
2. **Složkový config** (`.Tagger.!ftag` v projekt složce)
|
||||
- Ignore patterns
|
||||
- Custom tagy pro složku
|
||||
- Hardlink nastavení (output_dir, categories)
|
||||
- Rekurzivní skenování
|
||||
|
||||
3. **Souborové tagy** (`.filename.!tag`)
|
||||
- Tagy souboru
|
||||
- Datum
|
||||
- Stav (nové, ignorované)
|
||||
|
||||
### Klíčová pravidla
|
||||
|
||||
#### CO DĚLAT:
|
||||
|
||||
1. **UI NESMÍ obsahovat business logiku**
|
||||
```python
|
||||
# ❌ ŠPATNĚ
|
||||
class GUI:
|
||||
def save_file(self):
|
||||
with open(file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
# ✅ SPRÁVNĚ
|
||||
class GUI:
|
||||
def save_file(self):
|
||||
self.filemanager.save_file(file)
|
||||
```
|
||||
|
||||
2. **Core moduly NESMÍ importovat UI**
|
||||
```python
|
||||
# V src/core/*.py NIKDY:
|
||||
import tkinter
|
||||
from src.ui import anything
|
||||
```
|
||||
|
||||
3. **Dependency Injection - předávat dependencies přes konstruktor**
|
||||
```python
|
||||
class FileManager:
|
||||
def __init__(self, tagmanager: TagManager):
|
||||
self.tagmanager = tagmanager
|
||||
```
|
||||
|
||||
4. **UTF-8 encoding VŠUDE**
|
||||
```python
|
||||
with open(file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
```
|
||||
|
||||
5. **Type hints VŽDY**
|
||||
```python
|
||||
def filter_files(files: List[File], tags: List[Tag]) -> List[File]:
|
||||
pass
|
||||
```
|
||||
|
||||
#### CO NEDĚLAT:
|
||||
|
||||
1. **Globální stav** - NIKDY
|
||||
2. **Magic numbers** - použít konstanty
|
||||
3. **Ignorovat exceptions** - vždy logovat nebo ošetřit
|
||||
4. **Hardcoded paths** - použít Path
|
||||
|
||||
---
|
||||
|
||||
## Klíčové komponenty
|
||||
|
||||
### 1. Tag (immutable value object)
|
||||
|
||||
```python
|
||||
class Tag:
|
||||
def __init__(self, category: str, name: str):
|
||||
self.category = category # Nemění se po vytvoření!
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def full_path(self) -> str:
|
||||
return f"{self.category}/{self.name}"
|
||||
```
|
||||
|
||||
### 2. File (reprezentace souboru s metadaty)
|
||||
|
||||
```python
|
||||
class File:
|
||||
def __init__(self, file_path: Path, tagmanager=None):
|
||||
self.file_path = file_path
|
||||
self.filename = file_path.name
|
||||
self.metadata_filename = parent / f".{filename}.!tag"
|
||||
self.tags: list[Tag] = []
|
||||
self.date: str | None = None
|
||||
```
|
||||
|
||||
**Metadata format (.filename.!tag):**
|
||||
```json
|
||||
{
|
||||
"new": false,
|
||||
"ignored": false,
|
||||
"tags": ["Stav/Nové", "Video/HD"],
|
||||
"date": "2025-12-23"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. TagManager (správa tagů)
|
||||
|
||||
```python
|
||||
class TagManager:
|
||||
def __init__(self):
|
||||
self.tags_by_category = {} # {category: set(Tag)}
|
||||
# Automaticky načte výchozí tagy (Hodnocení, Barva)
|
||||
```
|
||||
|
||||
**Výchozí tagy:**
|
||||
- Hodnocení: 1-5 hvězd (exkluzivní výběr)
|
||||
- Barva: Červená, Modrá, Zelená, Žlutá, Oranžová
|
||||
|
||||
### 4. FileManager (správa souborů)
|
||||
|
||||
```python
|
||||
class FileManager:
|
||||
def __init__(self, tagmanager: TagManager):
|
||||
self.filelist: list[File] = []
|
||||
self.tagmanager = tagmanager
|
||||
self.on_files_changed = None # CALLBACK pro UI!
|
||||
self.global_config = load_global_config()
|
||||
self.folder_config = {}
|
||||
```
|
||||
|
||||
### 5. CSFD Scraper (filmové informace)
|
||||
|
||||
```python
|
||||
from src.core.csfd import fetch_movie, search_movies, CSFDMovie
|
||||
|
||||
# Načtení informací o filmu z URL
|
||||
movie = fetch_movie("https://www.csfd.cz/film/9423-pane-vy-jste-vdova/")
|
||||
print(movie.title) # „Pane, vy jste vdova!"
|
||||
print(movie.year) # 1970
|
||||
print(movie.rating) # 82
|
||||
print(movie.genres) # ['Komedie', 'Sci-Fi']
|
||||
print(movie.directors) # ['Václav Vorlíček']
|
||||
print(movie.actors) # ['Iva Janžurová', ...]
|
||||
|
||||
# Vyhledávání filmů
|
||||
results = search_movies("Pelíšky")
|
||||
for m in results:
|
||||
print(m.title, m.csfd_id)
|
||||
```
|
||||
|
||||
**CSFDMovie atributy:**
|
||||
- `title` - název filmu
|
||||
- `url` - CSFD URL
|
||||
- `year` - rok vydání
|
||||
- `genres` - seznam žánrů
|
||||
- `directors` - seznam režisérů
|
||||
- `actors` - seznam herců
|
||||
- `rating` - hodnocení v %
|
||||
- `rating_count` - počet hodnocení
|
||||
- `duration` - délka v minutách
|
||||
- `country` - země původu
|
||||
- `poster_url` - URL plakátu
|
||||
- `plot` - popis děje
|
||||
- `csfd_id` - ID filmu na CSFD
|
||||
|
||||
**Závislosti:** `requests`, `beautifulsoup4` (instalace: `poetry add requests beautifulsoup4`)
|
||||
|
||||
### 6. HardlinkManager (hardlink struktura)
|
||||
|
||||
```python
|
||||
class HardlinkManager:
|
||||
def __init__(self, output_dir: Path):
|
||||
self.output_dir = output_dir
|
||||
|
||||
def create_structure_for_files(files, categories=None) -> (success, fail)
|
||||
def find_obsolete_links(files, categories=None) -> List[(link, source)]
|
||||
def remove_obsolete_links(files, categories=None) -> (count, paths)
|
||||
def sync_structure(files, categories=None) -> (created, c_fail, removed, r_fail)
|
||||
```
|
||||
|
||||
**Příklad struktury:**
|
||||
```
|
||||
output/
|
||||
├── žánr/
|
||||
│ ├── Komedie/
|
||||
│ │ └── film.mkv (hardlink)
|
||||
│ └── Akční/
|
||||
│ └── film.mkv (hardlink)
|
||||
└── rok/
|
||||
└── 1988/
|
||||
└── film.mkv (hardlink)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GUI
|
||||
|
||||
### Moderní GUI (gui.py)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 📁 Otevřít │ 🔄 │ 🏷️ │ 📅 🔍 [____] Toolbar
|
||||
├────────────┬────────────────────────────────────────┤
|
||||
│ 📂 Štítky │ ☐ Plná │ Třídění: [Název] [▲] │
|
||||
│ ├─📁 Stav │ ┌──────────────────────────────────┐ │
|
||||
│ │ ☑ Nové │ │ Název│Datum│Štítky│Velikost │ │
|
||||
│ │ ☐ OK │ │file1 │2025 │HD │1.2 MB │ │
|
||||
│ ├─📁 Video│ │file2 │ │4K │15 MB │ │
|
||||
│ │ ☐ HD │ └──────────────────────────────────┘ │
|
||||
│ │ ☐ 4K │ │
|
||||
├────────────┴───────────────────────────────────────┤
|
||||
│ Připraven 3 vybráno │ 125 souborů │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Použít:** `poetry run python Tagger.py`
|
||||
|
||||
**Funkce:**
|
||||
- Tabulka s 4 sloupci (Název, Datum, Štítky, Velikost)
|
||||
- Toolbar s tlačítky
|
||||
- Keyboard shortcuts (Ctrl+O, Ctrl+T, F5, Del...)
|
||||
- Status bar se 3 sekcemi
|
||||
- Hromadné přiřazování tagů
|
||||
- Hardlink menu (Nástroje → Hardlink)
|
||||
|
||||
**Keyboard shortcuts:**
|
||||
- `Ctrl+O` - Otevřít složku
|
||||
- `Ctrl+Q` - Ukončit
|
||||
- `Ctrl+T` - Přiřadit tagy
|
||||
- `Ctrl+D` - Nastavit datum
|
||||
- `Ctrl+F` - Focus search
|
||||
- `F5` - Refresh
|
||||
- `Del` - Smazat z indexu
|
||||
|
||||
---
|
||||
|
||||
## Vývoj
|
||||
|
||||
### Setup prostředí
|
||||
|
||||
```bash
|
||||
# Poetry environment (VŽDY použij poetry!)
|
||||
poetry install
|
||||
poetry shell
|
||||
|
||||
# Nebo přímo:
|
||||
poetry run python Tagger.py
|
||||
```
|
||||
|
||||
### Testy
|
||||
|
||||
```bash
|
||||
# Všechny testy (189 testů)
|
||||
poetry run pytest tests/ -v
|
||||
|
||||
# S coverage
|
||||
poetry run pytest tests/ --cov=src/core --cov-report=html
|
||||
|
||||
# Konkrétní modul
|
||||
poetry run pytest tests/test_hardlink_manager.py -v
|
||||
|
||||
# Quick check
|
||||
poetry run pytest tests/ -q
|
||||
```
|
||||
|
||||
**Test coverage:** 100% core modulů
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Python Style
|
||||
|
||||
- **PEP 8** s výjimkami:
|
||||
- Max line length: **120** (ne 79)
|
||||
- Indentation: **4 mezery** (ne taby)
|
||||
- **UTF-8** encoding všude
|
||||
- **Type hints** povinné
|
||||
- **Docstrings** pro public API
|
||||
|
||||
### Imports Order
|
||||
|
||||
```python
|
||||
# 1. Standard library
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 2. Third-party
|
||||
import tkinter as tk
|
||||
from PIL import Image
|
||||
|
||||
# 3. Local
|
||||
from src.core.file import File
|
||||
from src.core.tag import Tag
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Branches
|
||||
|
||||
```
|
||||
main/master ← Production (NE commity přímo!)
|
||||
↑
|
||||
release ← Release candidate
|
||||
↑
|
||||
devel ← Development integration
|
||||
↑
|
||||
feature/* ← Feature branches ← VYVÍJÍME TADY
|
||||
```
|
||||
|
||||
### Commit Messages
|
||||
|
||||
```
|
||||
<type>: <subject>
|
||||
|
||||
[optional body]
|
||||
|
||||
🤖 Generated with Claude Code
|
||||
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat:` - Nová funkce
|
||||
- `fix:` - Bug fix
|
||||
- `refactor:` - Refactoring
|
||||
- `test:` - Testy
|
||||
- `docs:` - Dokumentace
|
||||
- `style:` - Formátování
|
||||
- `chore:` - Build, dependencies
|
||||
|
||||
---
|
||||
|
||||
## Plánované features
|
||||
|
||||
- [ ] Progress bar pro dlouhé operace
|
||||
- [ ] Undo/Redo mechanismus
|
||||
- [ ] Export do CSV/Excel
|
||||
- [ ] Dark mode theme
|
||||
- [ ] Drag & drop souborů
|
||||
- [ ] Image preview v sidebar
|
||||
- [ ] SQLite fallback pro >10k souborů
|
||||
- [ ] Full-text search
|
||||
|
||||
### Nice to have
|
||||
|
||||
- [ ] Plugin systém
|
||||
- [ ] Web interface (Flask)
|
||||
- [ ] Cloud sync (Dropbox, GDrive)
|
||||
- [ ] Batch rename podle tagů
|
||||
- [ ] Smart folder suggestions
|
||||
|
||||
---
|
||||
|
||||
## Metriky projektu
|
||||
|
||||
**Testy:** 274 (všechny ✅)
|
||||
**Test coverage:** 100% core modulů
|
||||
**Python verze:** 3.12+
|
||||
**Dependencies:** Pillow (PIL), requests, beautifulsoup4
|
||||
**Vývojové prostředí:** Poetry
|
||||
|
||||
**Performance:**
|
||||
- ✅ Dobré: <1000 souborů
|
||||
- ⚠️ Přijatelné: 1000-5000 souborů
|
||||
- ❌ Pomalé: >5000 souborů
|
||||
|
||||
---
|
||||
|
||||
## Debugování
|
||||
|
||||
### Časté problémy
|
||||
|
||||
**1. TreeView tagy se nezobrazují správně po načtení z CSFD**
|
||||
```
|
||||
# Opraveno: přidán update_idletasks() po refresh_sidebar()
|
||||
# Pokud stále přetrvává, zkuste F5 nebo znovu otevřít složku
|
||||
```
|
||||
|
||||
**2. "Cannot import ImageTk"**
|
||||
```bash
|
||||
# Řešení: Použij poetry environment
|
||||
poetry run python Tagger.py
|
||||
```
|
||||
|
||||
**2. "Config file not found"**
|
||||
```bash
|
||||
# Normální při prvním spuštění
|
||||
# Vytvoří se automaticky .Tagger.!gtag
|
||||
```
|
||||
|
||||
**3. "Metadata corrupted"**
|
||||
```python
|
||||
# V config.py je graceful degradation
|
||||
# Vrátí default config při chybě
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dokumentace
|
||||
|
||||
**AKTUÁLNÍ:**
|
||||
- **PROJECT_NOTES.md** - TENTO SOUBOR (single source of truth)
|
||||
- **CHANGELOG.md** - Historie verzí
|
||||
- Docstrings v kódu
|
||||
|
||||
---
|
||||
|
||||
## Pro AI asistenty (jako Claude)
|
||||
|
||||
### Když začínám práci na projektu:
|
||||
|
||||
1. **PŘEČTI TENTO SOUBOR CELÝ!**
|
||||
2. Zkontroluj `git status`
|
||||
3. Aktivuj poetry environment
|
||||
4. Spusť testy (`poetry run pytest tests/`)
|
||||
5. Dodržuj pravidla výše
|
||||
|
||||
### Při commitování:
|
||||
|
||||
1. Testy prošly (`pytest tests/`)
|
||||
2. Type hints přidány
|
||||
3. UTF-8 encoding
|
||||
4. Žádné TODO/FIXME
|
||||
5. Commit message formát správný
|
||||
|
||||
### Při přidání nové funkce:
|
||||
|
||||
1. Testy napsány PŘED implementací (TDD)
|
||||
2. Dokumentace aktualizována (TENTO SOUBOR!)
|
||||
3. Architecture decision zdokumentováno (pokud významné)
|
||||
4. Type hints všude
|
||||
5. Error handling přidán
|
||||
|
||||
---
|
||||
|
||||
## Kontakt & Help
|
||||
|
||||
**Autor:** honza
|
||||
**Repository:** /home/honza/Documents/Tagger
|
||||
**Python:** 3.12+
|
||||
**OS:** Linux
|
||||
|
||||
**Pro pomoc:**
|
||||
- Přečti TENTO soubor
|
||||
- Podívej se do testů (`tests/`)
|
||||
- Zkontroluj docstrings v kódu
|
||||
|
||||
---
|
||||
|
||||
**Last updated:** 2025-12-29
|
||||
**Maintainer:** Claude Opus 4.5 + honza
|
||||
15
Tagger.spec
15
Tagger.spec
@@ -19,27 +19,20 @@ pyz = PYZ(a.pure)
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Tagger',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
onefile=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='Tagger',
|
||||
)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB |
410
poetry.lock
generated
410
poetry.lock
generated
@@ -1,5 +1,17 @@
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.5"
|
||||
description = "Python graph (network) package"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"},
|
||||
{file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.14.3"
|
||||
@@ -25,14 +37,14 @@ lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
version = "2026.1.4"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
|
||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
||||
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
|
||||
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -198,117 +210,146 @@ files = [
|
||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.4"
|
||||
description = "Mach-O header analysis and editing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"darwin\""
|
||||
files = [
|
||||
{file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"},
|
||||
{file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
version = "26.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
|
||||
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2024.8.26"
|
||||
description = "Python PE parsing module"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"},
|
||||
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.0.0"
|
||||
version = "12.1.0"
|
||||
description = "Python Imaging Library (fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"},
|
||||
{file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"},
|
||||
{file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"},
|
||||
{file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"},
|
||||
{file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"},
|
||||
{file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"},
|
||||
{file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"},
|
||||
{file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"},
|
||||
{file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"},
|
||||
{file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"},
|
||||
{file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"},
|
||||
{file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"},
|
||||
{file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"},
|
||||
{file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"},
|
||||
{file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"},
|
||||
{file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"},
|
||||
{file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"},
|
||||
{file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"},
|
||||
{file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -350,6 +391,114 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.18.0"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.15,>=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9"},
|
||||
{file = "pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381"},
|
||||
{file = "pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2025.9"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
completion = ["argcomplete"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2026.0"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5"},
|
||||
{file = "pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyside6"
|
||||
version = "6.10.1"
|
||||
description = "Python bindings for the Qt cross-platform application and UI framework"
|
||||
optional = false
|
||||
python-versions = "<3.15,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516"},
|
||||
{file = "pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa"},
|
||||
{file = "pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c"},
|
||||
{file = "pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e"},
|
||||
{file = "pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
PySide6_Addons = "6.10.1"
|
||||
PySide6_Essentials = "6.10.1"
|
||||
shiboken6 = "6.10.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-addons"
|
||||
version = "6.10.1"
|
||||
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
|
||||
optional = false
|
||||
python-versions = "<3.15,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82"},
|
||||
{file = "pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4"},
|
||||
{file = "pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c"},
|
||||
{file = "pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341"},
|
||||
{file = "pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
PySide6_Essentials = "6.10.1"
|
||||
shiboken6 = "6.10.1"
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-essentials"
|
||||
version = "6.10.1"
|
||||
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
|
||||
optional = false
|
||||
python-versions = "<3.15,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe"},
|
||||
{file = "pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856"},
|
||||
{file = "pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2"},
|
||||
{file = "pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674"},
|
||||
{file = "pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
shiboken6 = "6.10.1"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
@@ -372,6 +521,19 @@ pygments = ">=2.7.2"
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
@@ -394,16 +556,52 @@ urllib3 = ">=1.21.1,<3"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "80.10.1"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "setuptools-80.10.1-py3-none-any.whl", hash = "sha256:fc30c51cbcb8199a219c12cc9c281b5925a4978d212f84229c909636d9f6984e"},
|
||||
{file = "setuptools-80.10.1.tar.gz", hash = "sha256:bf2e513eb8144c3298a3bd28ab1a5edb739131ec5c22e045ff93cd7f5319703a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyobjc (<12) ; sys_platform == \"darwin\" and python_version <= \"3.9\"", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
|
||||
type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "shiboken6"
|
||||
version = "6.10.1"
|
||||
description = "Python/C++ bindings helper module"
|
||||
optional = false
|
||||
python-versions = "<3.15,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71"},
|
||||
{file = "shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4"},
|
||||
{file = "shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0"},
|
||||
{file = "shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e"},
|
||||
{file = "shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.1"
|
||||
version = "2.8.3"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434"},
|
||||
{file = "soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350"},
|
||||
{file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"},
|
||||
{file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -420,14 +618,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
|
||||
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
|
||||
{file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
|
||||
{file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -438,5 +636,5 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "f6001ed675bdcc993bafb4f3170aa7bfc8aa86d75d370acf0063329fccfa7dd9"
|
||||
python-versions = ">=3.13,<3.15"
|
||||
content-hash = "81a84a97aa8532b37af24fe1ec6398f0a4cef1993e80e83ff16a5b571df344c6"
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "tagger"
|
||||
version = "1.0.4"
|
||||
description = "Universal file tagging utility"
|
||||
authors = ["Jan Doubravský <jan.doubravsky@gmail.com>"]
|
||||
version = "1.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
{name = "Jan Doubravský",email = "jan.doubravsky@gmail.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13,<3.15"
|
||||
dependencies = [
|
||||
"pillow (>=12.1.0,<13.0.0)",
|
||||
"requests (>=2.32.5,<3.0.0)",
|
||||
"beautifulsoup4 (>=4.14.3,<5.0.0)",
|
||||
"pyside6 (>=6.10.1,<7.0.0)"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
package-mode = false
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
pillow = "^12.0.0"
|
||||
requests = "^2.32.5"
|
||||
beautifulsoup4 = "^4.14.3"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^9.0.2"
|
||||
pyinstaller = "^6.18.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
|
||||
3
src/core/_version.py
Normal file
3
src/core/_version.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Auto-generated version file - do not edit manually
|
||||
# This file is updated from pyproject.toml when available
|
||||
VERSION = "1.1.0"
|
||||
@@ -1,4 +1,106 @@
|
||||
# src/core/constants.py
|
||||
VERSION = "v1.0.4"
|
||||
APP_NAME = "Tagger"
|
||||
APP_VIEWPORT = "1000x700"
|
||||
"""
|
||||
Application constants with dynamic version loading.
|
||||
|
||||
Version is loaded from pyproject.toml if available, otherwise from _version.py.
|
||||
If ENV_DEBUG=true in .env, " DEV" suffix is added to version.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
# Paths
|
||||
_ROOT_DIR = Path(__file__).parent.parent.parent
|
||||
_PYPROJECT_PATH = _ROOT_DIR / "pyproject.toml"
|
||||
_VERSION_FILE = Path(__file__).parent / "_version.py"
|
||||
_ENV_FILE = _ROOT_DIR / ".env"
|
||||
|
||||
|
||||
def _load_env_debug() -> bool:
|
||||
"""Load ENV_DEBUG from .env file."""
|
||||
if not _ENV_FILE.exists():
|
||||
return False
|
||||
try:
|
||||
with open(_ENV_FILE, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("ENV_DEBUG="):
|
||||
value = line.split("=", 1)[1].strip().lower()
|
||||
return value in ("true", "1", "yes")
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _extract_version_from_toml() -> str | None:
|
||||
"""Extract version from pyproject.toml."""
|
||||
if not _PYPROJECT_PATH.exists():
|
||||
return None
|
||||
try:
|
||||
with open(_PYPROJECT_PATH, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
# Simple parsing - find version = "x.x.x" in [project] section
|
||||
in_project = False
|
||||
for line in content.split("\n"):
|
||||
line = line.strip()
|
||||
if line == "[project]":
|
||||
in_project = True
|
||||
elif line.startswith("[") and in_project:
|
||||
break
|
||||
elif in_project and line.startswith("version"):
|
||||
# version = "1.0.4"
|
||||
if "=" in line:
|
||||
value = line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
return value
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _load_version_from_file() -> str:
|
||||
"""Load version from _version module."""
|
||||
try:
|
||||
from src.core._version import VERSION as _ver
|
||||
return _ver
|
||||
except ImportError:
|
||||
pass
|
||||
return "0.0.0"
|
||||
|
||||
|
||||
def _save_version_to_file(version: str) -> None:
|
||||
"""Save version to _version.py for fallback."""
|
||||
try:
|
||||
content = f'''# Auto-generated version file - do not edit manually
|
||||
# This file is updated from pyproject.toml when available
|
||||
VERSION = "{version}"
|
||||
'''
|
||||
with open(_VERSION_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _get_version() -> str:
|
||||
"""Get version from pyproject.toml or fallback to _version.py."""
|
||||
# Try to get from pyproject.toml
|
||||
toml_version = _extract_version_from_toml()
|
||||
if toml_version:
|
||||
# Update _version.py for cases when toml is not available
|
||||
_save_version_to_file(toml_version)
|
||||
return toml_version
|
||||
|
||||
# Fallback to _version.py
|
||||
return _load_version_from_file()
|
||||
|
||||
|
||||
# Load configuration
|
||||
DEBUG = _load_env_debug()
|
||||
VERSION = _get_version()
|
||||
|
||||
# Add DEV suffix if debug mode
|
||||
if DEBUG:
|
||||
VERSION = f"{VERSION} DEV"
|
||||
|
||||
# Application name with version
|
||||
APP_NAME = f"Tagger v{VERSION}"
|
||||
|
||||
# Default window size
|
||||
APP_VIEWPORT = "1000x700"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Module header
|
||||
import sys
|
||||
import subprocess
|
||||
from loguru import logger
|
||||
from .file import File
|
||||
from .tag_manager import TagManager
|
||||
|
||||
@@ -8,21 +9,13 @@ if __name__ == "__main__":
|
||||
sys.exit("This module is not intended to be executed as the main program.")
|
||||
|
||||
|
||||
# Backwards compatibility: load_icon moved to src/ui/utils.py
|
||||
def load_icon(path):
|
||||
"""Deprecated: Use src.ui.utils.load_icon instead."""
|
||||
from src.ui.utils import load_icon as _load_icon
|
||||
return _load_icon(path)
|
||||
|
||||
|
||||
def add_video_resolution_tag(file_obj: File, tagmanager: TagManager):
|
||||
def add_video_resolution_tag(file_obj: File, tagmanager: TagManager) -> None:
|
||||
"""
|
||||
Zjistí vertikální rozlišení videa a přiřadí tag Rozlišení/{výška}p.
|
||||
Vyžaduje ffprobe (FFmpeg).
|
||||
Detect video vertical resolution and assign tag Resolution/{height}p.
|
||||
Requires ffprobe (FFmpeg).
|
||||
"""
|
||||
path = str(file_obj.file_path)
|
||||
try:
|
||||
# ffprobe vrátí width a height ve formátu JSON
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||
"-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", path],
|
||||
@@ -30,13 +23,13 @@ def add_video_resolution_tag(file_obj: File, tagmanager: TagManager):
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
res = result.stdout.strip() # např. "1920x1080"
|
||||
res = result.stdout.strip() # e.g. "1920x1080"
|
||||
if "x" not in res:
|
||||
return
|
||||
width, height = map(int, res.split("x"))
|
||||
tag_name = f"Rozlišení/{height}p"
|
||||
tag_obj = tagmanager.add_tag("Rozlišení", f"{height}p")
|
||||
file_obj.add_tag(tag_obj)
|
||||
print(f"Přiřazen tag {tag_name} k {file_obj.filename}")
|
||||
logger.info("Assigned tag {} to {}", tag_name, file_obj.filename)
|
||||
except Exception as e:
|
||||
print(f"Chyba při získávání rozlišení videa {file_obj.filename}: {e}")
|
||||
logger.error("Failed to get video resolution for {}: {}", file_obj.filename, e)
|
||||
|
||||
1733
src/ui/gui.py
1733
src/ui/gui.py
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,22 @@
|
||||
"""
|
||||
UI utility functions for Tagger GUI.
|
||||
UI utility functions for Tagger GUI (PySide6).
|
||||
"""
|
||||
from PIL import Image, ImageTk
|
||||
from pathlib import Path
|
||||
from PySide6.QtGui import QIcon, QPixmap
|
||||
|
||||
|
||||
def load_icon(path) -> ImageTk.PhotoImage:
|
||||
def load_icon(path: str | Path, size: int = 16) -> QIcon:
|
||||
"""
|
||||
Load an icon from file and resize to 16x16.
|
||||
Load an icon from file and optionally resize.
|
||||
|
||||
Args:
|
||||
path: Path to the image file
|
||||
size: Icon size in pixels (default 16)
|
||||
|
||||
Returns:
|
||||
ImageTk.PhotoImage resized to 16x16 pixels
|
||||
QIcon object
|
||||
"""
|
||||
img = Image.open(path)
|
||||
img = img.resize((16, 16), Image.Resampling.LANCZOS)
|
||||
return ImageTk.PhotoImage(img)
|
||||
pixmap = QPixmap(str(path))
|
||||
if not pixmap.isNull():
|
||||
pixmap = pixmap.scaled(size, size)
|
||||
return QIcon(pixmap)
|
||||
|
||||
@@ -1,62 +1,76 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
import os
|
||||
|
||||
from src.ui.utils import load_icon
|
||||
from PIL import Image, ImageTk
|
||||
import tkinter as tk
|
||||
# Skip all tests if no display is available (CI environment)
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("DISPLAY") is None and os.environ.get("WAYLAND_DISPLAY") is None,
|
||||
reason="No display available for GUI tests"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def tk_root():
|
||||
"""Fixture pro inicializaci Tkinteru (nutné pro ImageTk)."""
|
||||
root = tk.Tk()
|
||||
yield root
|
||||
root.destroy()
|
||||
def qapp():
|
||||
"""Fixture to initialize QApplication for Qt tests."""
|
||||
from PySide6.QtWidgets import QApplication
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication([])
|
||||
yield app
|
||||
|
||||
|
||||
def test_load_icon_returns_photoimage(tk_root):
|
||||
"""Test že load_icon vrací PhotoImage"""
|
||||
# vytvoříme dočasný obrázek
|
||||
def test_load_icon_returns_qicon(qapp):
|
||||
"""Test that load_icon returns QIcon"""
|
||||
from src.ui.utils import load_icon
|
||||
from PySide6.QtGui import QIcon
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
# vytvoříme 100x100 červený obrázek
|
||||
# Create 100x100 red image
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
img.save(tmp_path)
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
|
||||
# musí být PhotoImage
|
||||
assert isinstance(icon, ImageTk.PhotoImage)
|
||||
|
||||
# ověříme velikost 16x16
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
# Must be QIcon
|
||||
assert isinstance(icon, QIcon)
|
||||
# Icon should not be null
|
||||
assert not icon.isNull()
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_load_icon_resizes_image(tk_root):
|
||||
"""Test že load_icon správně změní velikost obrázku"""
|
||||
def test_load_icon_custom_size(qapp):
|
||||
"""Test that load_icon respects custom size parameter"""
|
||||
from src.ui.utils import load_icon
|
||||
from PIL import Image
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
# vytvoříme velký obrázek 500x500
|
||||
img = Image.new("RGB", (500, 500), color="blue")
|
||||
img.save(tmp_path)
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
icon = load_icon(tmp_path, size=32)
|
||||
|
||||
# i velký obrázek by měl být zmenšen na 16x16
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
# Icon should be created successfully
|
||||
assert not icon.isNull()
|
||||
# Available sizes should include the requested size
|
||||
sizes = icon.availableSizes()
|
||||
assert len(sizes) > 0
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_load_icon_different_formats(tk_root):
|
||||
"""Test načítání různých formátů obrázků"""
|
||||
def test_load_icon_different_formats(qapp):
|
||||
"""Test loading different image formats"""
|
||||
from src.ui.utils import load_icon
|
||||
from PySide6.QtGui import QIcon
|
||||
from PIL import Image
|
||||
|
||||
formats = [".png", ".jpg", ".bmp"]
|
||||
|
||||
for fmt in formats:
|
||||
@@ -68,8 +82,7 @@ def test_load_icon_different_formats(tk_root):
|
||||
|
||||
icon = load_icon(tmp_path)
|
||||
|
||||
assert isinstance(icon, ImageTk.PhotoImage)
|
||||
assert icon.width() == 16
|
||||
assert icon.height() == 16
|
||||
assert isinstance(icon, QIcon)
|
||||
assert not icon.isNull()
|
||||
finally:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user