55 Commits

Author SHA1 Message Date
d0ca9cc1cc add qifdp lib 2025-12-09 03:11:41 +01:00
cb45f67fff some stuff idk what I've done lol 2025-07-20 02:39:03 +02:00
1486203dd2 fix date string lenght 2025-07-15 22:51:43 +02:00
dad7a26544 cleanup 2025-07-02 01:39:18 +02:00
5d3745a12c add UV 2025-07-02 00:54:15 +02:00
99a5c7c110 good night yall 2024-06-05 01:43:22 +02:00
c4fea238b3 fix bug when meta tags aren't in the correct order 2024-06-05 01:22:35 +02:00
f4ace7efa3 installer 2024-05-26 23:25:59 +02:00
0dea53d8f5 added more translations: fr, it, ru, uk 2024-05-25 23:28:32 +02:00
c283e0221a Merge branch 'develop' into 'main'
develop

See merge request DasMoorhuhn/autopicture-v3!7
2024-05-25 21:18:24 +00:00
c051e120fe changelog and version update 2024-05-25 23:16:38 +02:00
cc3f0ea89b Fix CI 2024-05-25 23:04:05 +02:00
161525fbb5 i18n, tests and more 2024-05-25 22:42:29 +02:00
c21167f941 mime types 2024-01-13 20:06:04 +01:00
892df0144e Merge remote-tracking branch 'origin/main' into develop 2023-12-21 14:23:04 +01:00
aaf7060252 added new file for development modules 2023-12-21 14:22:09 +01:00
0660628647 Modified README 2023-12-21 14:02:48 +01:00
e6c59507c4 wqadded new way to get mime type 2023-12-21 13:54:24 +01:00
f8383567fe changed to dict 2023-12-21 11:27:29 +01:00
a869b60933 Merge branch 'develop' into 'main'
develop

See merge request DasMoorhuhn/autopicture-v3!6
2023-12-17 20:37:28 +00:00
3c8974ee74 activate apple test 2023-12-17 21:36:32 +01:00
cdf8948193 did more tests 2023-12-17 21:15:06 +01:00
f2fd803b57 yeet 2023-12-17 19:31:35 +01:00
033eb269f0 Merge branch 'develop' into 'main'
Develop

See merge request DasMoorhuhn/autopicture-v3!5
2023-12-15 00:12:53 +00:00
480b97059d made tests run 2023-12-15 01:01:29 +01:00
e5ed2e7319 raise to python3.12 2023-12-15 01:00:31 +01:00
0f22b70e06 fixture versions 2023-12-12 22:56:03 +01:00
ffdf3f3779 added more unit tests 2023-12-12 22:37:15 +01:00
4d8c0c1dda added testfiles for iphone 2023-12-12 22:26:28 +01:00
6bea0ee524 seperated main 2023-12-12 21:56:30 +01:00
5d7ca9172b raise to python3.11 2023-12-12 21:22:21 +01:00
0f6601ecd9 removed round 2023-12-12 03:36:01 +01:00
f32822c861 ci only on main 2023-12-12 03:33:15 +01:00
52b776d495 Merge branch 'develop' into 'main'
try coverage

See merge request DasMoorhuhn/autopicture-v3!4
2023-12-12 02:31:17 +00:00
941aa141af try coverage 2023-12-12 03:30:40 +01:00
3e0a40eee9 fix coverage 2023-12-12 02:36:15 +01:00
c774b8428c fix coverage 2023-12-12 02:34:09 +01:00
d440f3ff1d .. 2023-12-12 02:31:41 +01:00
6b4e8df4a5 .. 2023-12-12 02:20:46 +01:00
a5160aa0c9 .. 2023-12-12 02:19:08 +01:00
250f7b6554 .. 2023-12-12 02:14:46 +01:00
d67b6f5669 Merge branch 'develop' into 'main'
Develop

See merge request DasMoorhuhn/autopicture-v3!3
2023-12-12 00:46:51 +00:00
968c549bbe CI 2023-12-12 01:46:19 +01:00
0cf66c5781 added test files 2023-12-12 00:35:36 +01:00
fc9ce09e0f .. 2023-12-08 01:19:00 +01:00
c6bda88974 excluded updater because not needed atm 2023-12-08 01:17:16 +01:00
4544681f45 Merge branch 'develop' into 'main'
Develop

See merge request DasMoorhuhn/autopicture-v3!2
2023-12-08 00:14:49 +00:00
27c5cd5076 more test stuff 2023-12-08 01:14:01 +01:00
0012c52adf 54 percent... wow 2023-12-08 00:44:12 +01:00
9ae71af1aa more tests 2023-12-07 22:27:05 +01:00
3ebe5bba7f fixed test call 2023-12-07 22:12:56 +01:00
529776bac6 start making tests 2023-12-07 21:58:42 +01:00
d9ac0072ac included all files 2023-12-07 20:38:01 +01:00
2da422abc5 make pytests 2023-12-07 19:59:51 +01:00
cb9e02f4a2 added tests and scan for releases 2023-12-01 01:26:38 +01:00
65 changed files with 1131 additions and 106 deletions

3
.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
source = src
omit = tests/*, __init__.py, updater.py, main.py

5
.gitignore vendored
View File

@@ -1,4 +1,9 @@
__pycache__/ __pycache__/
.idea/ .idea/
app/ app/
.test_folder/
*/coverage/
*.log *.log
*.xml
.coverage

13
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,13 @@
pytest:
image: python:3.12-alpine
script:
- sh tests/start_tests_gitlab.sh
# - sed -i "s#<source>/builds/DasMoorhuhn/autopicture-v3/src</source>#<source>${CI_PROJECT_DIR}</source>#g" coverage.xml
coverage: '/Code coverage: \d+(?:\.\d+)?/'
artifacts:
name: "$CI_JOB_NAME"
reports:
junit: report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -1,3 +1,11 @@
## v0.2.0 []
- Added i18n for multi language support
- Added en, fr, it, ru and uk as language. en is set to default in `config.yml`
- Fixed tests
- Fixed gitlab CI file
- Fixed bug on string splitting at filtering date/time
- Only pictures will be sorted
- WIP: RAW sorting
## v0.1.0 [2023-11-30] ## v0.1.0 [2023-11-30]
- Added updater - Added updater
- Added Changelog file - Added Changelog file

View File

@@ -1,6 +1,77 @@
# AutoPicture V3 # AutoPicture V3
Dies ist die dritte Version von AutoPicture. Picture sorting software written in python3.
# Erste Schritte Example structure:
```bash
app/Bilder/
└── 2023
├── 10
│   ├── 21
│   │   ├── DSC02975.JPG
│   │   └── DSC02976.JPG
│   ├── 25
│   │   ├── DSC03030.JPG
│   │   ├── DSC03031.JPG
│   │   └── DSC03096.JPG
│   ├── 28
│   │   ├── DSC03097.JPG
│   │   ├── DSC03098.JPG
│   │   └── DSC03116.JPG
│   ├── 29
│   │   ├── DSC03117.JPG
│   │   ├── DSC03118.JPG
│   │   └── DSC03135.JPG
│   └── 30
│   ├── DSC03136.JPG
│   └── DSC03137.JPG
├── 11
│   ├── 16
│   │   └── DSC03144.JPG
│   ├── 17
│   │   ├── DSC03145.JPG
│   │   └── DSC03146.JPG
│   └── 28
│   ├── DSC03153.JPG
│   ├── DSC03154.JPG
│   └── DSC03155.JPG
├── ...
```
# Setup
## Python
### Linux
Debian:
```bash
sudo apt-get install -y python3 python3-pip
```
### Windows
TODO:
Download the latest version of Python and install it
## Pip
```bash
pip3 install uv
```
## Config
## Starten
```shell
uv run ...
```
# Tests
```bash
sh ./tests/start_tests.sh
```

4
develop_requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
pytest
pytest-cov
pytest-factoryboy
gitlabci-local

View File

@@ -1 +1,50 @@
# TODO import distutils.text_file
import platform
import os
import shutil
import pkg_resources
WORKDIR = os.getcwd()
OS_SUPPORTED = platform.system() in ['Linux', 'Mac', 'Windows']
PYTHON_SUPPORTED = platform.python_version() >= '3.10'
def install_linux():
install_path = os.path.join('~', '.local', 'share', 'AutoPicture_v3')
if os.path.exists(install_path): exit('Sorry, this software is already installed')
# shutil.copy()
def install_mac():
pass
def install_windows():
pass
def check_pip():
pkg_resources.require(open(os.path.join(WORKDIR,'requirements.txt' ), mode='r'))
def check_supported_host():
print(f'Detected OS: {platform.system()}')
print(f'Detected Python: {platform.python_version()}')
print()
print('OS Supported: OK') if OS_SUPPORTED else print('OS Supported: NO')
print('Python supported: OK') if PYTHON_SUPPORTED else print('Python supported: NO')
check_supported_host()
check_pip()
match platform.system():
case 'Linux': install_linux()
case 'Mac': install_mac()
case 'Windows': install_windows()
case _: print("Your system is not supported.")

1
install.sh Normal file
View File

@@ -0,0 +1 @@
sudo apt-get install -y python3 python3-pip

11
install/AutoPictureV3.desktop Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env xdg-open
[Desktop Entry]
Type=Application
Encoding=UTF-8
Name=AutoPicture v3
Comment=Picture sorting software written in python3
Icon=/path/to/icon.xpm
Exec=sh ~/.local/share/AutoPictureV3/run.sh
Terminal=True
Categories=Picture;Sorting

20
pyproject.toml Normal file
View File

@@ -0,0 +1,20 @@
[project]
name = "autopicture-v3"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"filetype>=1.2.0",
"pillow>=11.3.0",
"progressbar>=2.5",
"pytest>=9.0.2",
"python-i18n[yaml]>=0.3.9",
"python-magic>=0.4.27",
"pyyaml>=6.0.2",
"qifdp",
"requests>=2.32.4",
]
[tool.uv.sources]
qifdp = { url = "https://git.pinguin-software.de/DasMoorhuhn/qifdp/releases/download/25.12.9/qifdp-25.12.9-py3-none-any.whl" }

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
pythonpath = tests
addopts = --ignore-glob=**/tests/**

View File

@@ -1,5 +1,9 @@
pillow pillow
python-magic pyyaml
python-magi
progressbar progressbar
virtualenv virtualenv
requests requests
filetype
python-i18n[YAML]
pyinstaller

View File

@@ -1,4 +1,4 @@
{ {
"version": "0.1.0", "version": "0.2.0",
"date": "2023-11-30" "date": "2024-05-25"
} }

0
src/__init__.py Normal file
View File

18
src/config.py Normal file
View File

@@ -0,0 +1,18 @@
import yaml
import os
from pathlib import Path
workdir, filename = os.path.split(os.path.abspath(__file__))
config_file = os.path.join(Path.home(), 'config.yml')
def get_config():
with open(file=config_file, mode='r') as file:
return Config(yaml.safe_load(file))
class Config:
def __init__(self, data):
self.src = data['src']
self.dst = data['dst']
self.language = data['language']

11
src/config.yml Normal file
View File

@@ -0,0 +1,11 @@
# src (source) is the path, were the images are, that should be sorted
src: "~/Bilder/tmp"
# dst (destination) ist the path, were the images are going to be sorted
dst: "~/Bilder/sort"
# de, en, fr, it, ru, uk
language: "en"
# 1: only log the file transfers, 2: ..., 3: ...
logging: 1

View File

@@ -1,10 +1,15 @@
class ExifData: class ExifData:
"""This is for an object that stores the data of a picture""" """This is for an object that stores the data of a picture"""
def __init__(self, image_path, image_name, day, month, year, time, make) -> None: def __init__(self, data:dict) -> None:
self.path = image_path self.path:str = str(data['image_path'])
self.name = image_name self.name:str = str(data['image_name'])
self.day = int(day) self.day:str = str(data['day'])
self.month = int(month) self.month:str = str(data['month'])
self.year = int(year) self.year:str = str(data['year'])
self.time = str(time) self.time:str = str(data['time'])
self.make = str(make) # self.make:str = str(data['make'])
# fix date strings so they are always 2 chars long
if len(self.day) < 2: self.day = "0" + self.day
if len(self.month) < 2: self.month = "0" + self.month
if len(self.year) < 2: self.year = "0" + self.year

View File

@@ -1,37 +1,40 @@
import os import os
import time import sys
sys.path.append("../")
import shutil import shutil
import logging import logging
from progressbar.progressbar import ProgressBar from progressbar.progressbar import ProgressBar
from exif_data import ExifData from src.exif_data import ExifData
def sort_pictures(images:list, dst:str, logger:logging.Logger): def sort_pictures(images:list, dst:str, logger:logging.Logger):
image_total = len(images) image_total = len(images)
image_counter = 0 image_counter = 0
logging_infos = []
progress_bar = ProgressBar( progress_bar = ProgressBar(
maxval=image_total, maxval=image_total,
term_width=70 term_width=70
) )
print(f"Start sorting {image_total} images\n")
progress_bar.start() progress_bar.start()
start_timer = time.time()
for image in images: for image in images:
image:ExifData image:ExifData
path = os.path.join(dst, str(image.make), str(image.year), str(image.month), str(image.day)) if not image: continue
if not os.path.exists(path): os.makedirs(path) path = os.path.join(dst, str(image.year), str(image.month), str(image.day))
image_dst = os.path.join(path, image.name)
if not os.path.exists(path): os.makedirs(path, exist_ok=True)
stat_info = os.stat(image.path) stat_info = os.stat(image.path)
shutil.move(src=image.path, dst=f"{path}/{image.name}") shutil.move(src=image.path, dst=image_dst)
# os.chmod(path=f"{path}/{image.name}", mode=stat_info.st_mode) # os.chmod(path=f"{path}/{image.name}", mode=stat_info.st_mode)
logger.info(f"Moved {image.path} -> {path}/{image.name}") logger.info(f"Moved {image.path} -> {image_dst}")
progress_bar.update(image_counter) progress_bar.update(image_counter)
image_counter += 1 image_counter += 1
end_timer = time.time()
progress_bar.finish() progress_bar.finish()
print(f"\nDone\nSorted {image_total} images in {round(end_timer - start_timer, 2)} seconds")
def sort_raws(raws:list, dst:str, logger:logging):
pass

View File

@@ -0,0 +1,5 @@
de:
start_sorting_images: 'Sortierung von %{image_count} Bild(ern) wird gestartet'
done_sorting_images: '%{image_count} Bild(er) in %{time} Sekunden sortiert'
done: 'Fertig'
no_images_found: 'Es wurden keine Bilder gefunden'

View File

@@ -0,0 +1,5 @@
en:
start_sorting_images: 'Start sorting %{image_count} image(s)'
done_sorting_images: '%{image_count} image(s) sorted in %{time} seconds'
done: 'Done'
no_images_found: 'No images found'

View File

@@ -0,0 +1,5 @@
fr:
start_sorting_images: 'Tri de %{image_count} image(s) en cours'
done_sorting_images: '%{image_count} image(s) triée(s) en %{time} secondes'
done: 'Terminé'
no_images_found: 'Aucune image trouvée'

View File

@@ -0,0 +1,5 @@
it:
start_sorting_images: 'Ordinamento di %{image_count} immagine(i) avviato'
done_sorting_images: '%{image_count} immagine(i) ordinate in %{time} secondi'
done: 'Fatto'
no_images_found: 'Nessuna immagine trovata'

View File

@@ -0,0 +1,5 @@
ru:
start_sorting_images: 'Начата сортировка %{image_count} изображения(ий)'
done_sorting_images: '%{image_count} изображение(ий) отсортировано за %{time} секунд'
done: 'Готово'
no_images_found: 'Изображения не найдены'

View File

@@ -0,0 +1,5 @@
uk:
start_sorting_images: 'Розпочато сортування %{image_count} зображення(ь)'
done_sorting_images: '%{image_count} зображення(ь) відсортовано за %{time} секунд'
done: 'Готово'
no_images_found: 'Зображень не знайдено'

View File

@@ -1,14 +1,9 @@
import sys import sys
import logging import logging
from process import start_process
from meta_data_handler import MetadataHandler
from file_handler import sort_pictures
from folder_handler import *
sys.path.append("../") sys.path.append("../")
log_folder = "." log_folder = "."
src = "../app/TempPic"
dst = "../app/Bilder"
logger = logging.getLogger('AutoPicture') logger = logging.getLogger('AutoPicture')
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@@ -16,21 +11,4 @@ handler = logging.FileHandler(filename=f'{log_folder}/AutoPicture.log', encoding
handler.setFormatter(logging.Formatter('%(asctime)s|:%(message)s')) handler.setFormatter(logging.Formatter('%(asctime)s|:%(message)s'))
logger.addHandler(handler) logger.addHandler(handler)
metadata_handler = MetadataHandler() start_process(logger=logger)
def start_process():
try:
exif_data = metadata_handler.get_meta_data(images=files)
sort_pictures(images=exif_data, dst=dst, logger=logger)
except Exception as err:
print(err)
logger.error(err)
raise err
files = recursive_scan_dir(src)
if len(files) > 0:
start_process()
else:
print("No images found")

View File

@@ -1,63 +1,109 @@
import magic import magic
import sys
import os
import filetype
from PIL import Image from PIL import Image
from PIL import ExifTags from PIL import ExifTags
from qifdp import IFDTagMap
from qifdp import get_raw_ifd_tag
from exif_data import ExifData sys.path.append("../")
from src.exif_data import ExifData
from src.mime_types import MimeTypes
video_formats = ["MP4", "MOV", "M4V", "MKV", "AVI", "WMV", "AVCHD", "WEBM", "MPEG"]
picture_formats = ["JPG", "JPEG", "PNG"]
# Only ARW will be supported as for now...
# raw_formats = ["CR2", "RAF", "RW2", "ERF", "NRW", "NEF", "ARW", "RWZ", "EIP",
# "DNG", "BAY", "DCR", "GPR", "RAW", "CRW", "3FR", "SR2", "K25",
# "KC2", "MEF", "DNG", "CS1", "ORF", "MOS", "KDC", "CR3", "ARI",
# "SRF", "SRW", "J6I", "FFF", "MRW", "MFW", "RWL", "X3F", "PEF",
# "IIQ", "CXI", "NKSC", "MDC"]
raw_formats = ["ARW"]
class MetadataHandler: def is_file_video(path:str):
"""This class is for getting the meta data from a image or a video""" mime = magic.Magic(mime=True)
file = mime.from_file(path)
if file.find('video') != -1: return True
else: return False
def __init__(self) -> None:
self.__videoFormats = ["MP4", "MOV", "M4V", "MKV", "AVI", "WMV", "AVCHD", "WEBM", "MPEG"]
self.__pictureFormats = ["JPG", "JPEG", "PNG", "TIFF"]
self.__keyWords = ["DateTime", "Make"]
def __is_file_video(self, path:str): def is_file_picture(path:str):
mime = magic.Magic(mime=True) mime = magic.Magic(mime=True)
file = mime.from_file(path) file = mime.from_file(path)
if file.find('video') != -1: return True if file.find('picture') != -1: return True
else: return False else: return False
def __get_image_meta_data(self, image_path):
image_extension = str(image_path).split("/")[-1].split(".")
# TODO: Sort out videos
img = Image.open(f"{image_path}")
values = []
for tag, text in img.getexif().items():
if tag in ExifTags.TAGS:
if image_extension[1].upper() in self.__pictureFormats:
values.append(ExifTags.TAGS[tag] + "|" + str(text))
return self.__filter_date_and_make(metaTags=self.__filter_data(value=values), imagePath=image_path)
def __filter_date_and_make(self, metaTags: list, imagePath): def handle_raw(image:str):
day = None # image_creation_time = os.path.getctime(filename=image)
month = None # print(image_creation_time)
year = None try:image_creation_time = get_raw_ifd_tag(file_path=image, read_buffer=0x100000)
time = None except:return
make = str(metaTags[1]).split("|")[1] date_time = image_creation_time.split(' ') # 'YYYY:MM:DD H:M:S'
image_name = str(imagePath).split("/")[-1] image_name = str(image).split(os.sep)[-1]
date, time = date_time[0].split(':'), date_time[1]
year, month, day = date[0], date[1], date[2]
_date = str(metaTags[0]).split("|") exif_data_dict = {
time = _date[1].split(" ")[1] "day": day,
_date = _date[1].split(" ")[0].split(":") "month": month,
day = _date[2] "year": year,
month = _date[1] "time": time,
year = _date[0] "image_path": image,
"image_name": image_name
}
return ExifData(image_path=imagePath, image_name=image_name, day=day, month=month, year=year, time=time, make=make) return ExifData(exif_data_dict)
def __filter_data(self, value):
value_return = []
for k in self.__keyWords:
for v in value:
temp = v.split("|")
if temp[0] == k:
value_return.append(v)
return value_return
def get_meta_data(self, images: list): def handle_image(image:str):
exif_data_list = [] img = Image.open(image)
for image in images: values = []
exif_data_list.append(self.__get_image_meta_data(image_path=image)) for tag, text in img.getexif().items(): values.append([ExifTags.TAGS[tag], str(text)]) if tag in ExifTags.TAGS else {}
return exif_data_list return filter_date_and_make(values, image_path=image)
def handle_video(video:str):
pass
def get_image_meta_data(image_path):
image_extension = str(image_path).split(os.sep)[-1].split('.')[1].upper()
# TODO: Sort out videos using mime type of file
# mime = MimeTypes(file_path=image_path)
if image_extension in picture_formats: return handle_image(image=image_path)
elif image_extension in video_formats: return handle_video(video=image_path)
elif image_extension in raw_formats: return handle_raw(image=image_path)
else: return
def filter_date_and_make(meta_tags:list, image_path:str):
make = next((tag[1] for tag in meta_tags if tag[0] == 'Make'), None)
date_time = next((tag[1] for tag in meta_tags if tag[0] == 'DateTime'), None)
date_time = date_time.split(' ') # 'YYYY:MM:DD H:M:S'
image_name = str(image_path).split(os.sep)[-1]
date, time = date_time[0].split(':'), date_time[1]
year, month, day = date[0], date[1], date[2]
exif_data_dict = {
"day": day,
"month": month,
"year": year,
"make": make,
"time": time,
"image_path": image_path,
"image_name": image_name
}
return ExifData(exif_data_dict)
def get_meta_data(images: list):
exif_data_list = []
for image in images:
exif_data_list.append(get_image_meta_data(image_path=image))
return exif_data_list

18
src/mime_types.py Normal file
View File

@@ -0,0 +1,18 @@
import filetype
class MimeTypes:
def __init__(self, file_path):
self.is_image = False
self.is_video = False
self.is_raw = False
self.is_unsupported_file_type = False
self.__proceed(file_path)
def __proceed(self, file_path):
if filetype.is_image(file_path):
self.is_image = True
elif filetype.is_video(file_path):
self.is_video = True
else:
self.is_unsupported_file_type = True

43
src/process.py Normal file
View File

@@ -0,0 +1,43 @@
import os
import sys
import i18n
sys.path.append("../")
from time import time
from src.meta_data_handler import get_meta_data
from src.file_handler import sort_pictures
from src.scan_folder import recursive_scan_folder
from src.config import get_config
workdir, filename = os.path.split(os.path.abspath(__file__))
config = get_config()
i18n.set('locale', config.language)
i18n.set('fallback', 'en')
i18n.set('filename_format', '{locale}.{format}')
i18n.load_path.append(f'{workdir}{os.sep}i18n_translations{os.sep}')
def start_process(logger):
try:
files = recursive_scan_folder(config.src)
if len(files) > 0:
start_timer = time()
exif_data = get_meta_data(images=files)
image_total = len(exif_data)
print(i18n.t('start_sorting_images', image_count=image_total))
sort_pictures(images=exif_data, dst=config.dst, logger=logger)
end_timer = time()
duration = round(end_timer - start_timer, 2)
print(i18n.t('done'))
print(i18n.t('done_sorting_images', time=duration, image_count=image_total))
return True
else:
print(i18n.t('no_images_found'))
return False
except Exception as err:
print(err)
logger.error(err)
raise err

View File

@@ -1,11 +1,11 @@
import os import os
def scan_dir(path:str): def scan_folder(path:str):
return next(os.walk(path))[2] return next(os.walk(path))[2]
def recursive_scan_dir(path:str): def recursive_scan_folder(path:str):
results = [] results = []
for root, folders, files in os.walk(path): for root, folders, files in os.walk(path):
list_files = os.listdir(root) list_files = os.listdir(root)

View File

@@ -1,18 +1,69 @@
import requests import requests
import json import json
# Proof of concept
class Version:
def __init__(self, data:dict):
self.version:str = data['version']
self.version_int:int = int(self.version.replace(".", ""))
self.date:str = data['date']
class Release:
def __init__(self, data:dict):
self.name = data['name']
self.tag_name = data['tag_name']
self.description = data['description']
self.created_at = data['created_at']
self.released_at = data['released_at']
self.upcoming_release = data['upcoming_release']
self.version_int = int(self.tag_name.replace(".", ""))
self.zip_file_url = data['assets']['sources']
self.__proceed()
def __proceed(self):
for assest in self.zip_file_url:
if assest['format'] == 'zip':
self.zip_file_url = assest['url']
break
def read_version(): def read_version():
with open(file=".version.json") as file: with open(file=".version.json") as file:
version = json.load(file) version = Version(json.load(file))
return version return version
def install():
pass
def check_for_update(): def check_for_update():
request = "https://gitlab.com/DasMoorhuhn/autopicture-v3/-/raw/main/src/.version.json" version_current = read_version()
response = requests.get(request) request = "https://gitlab.com/api/v4/projects/52637155/releases"
response = requests.get(url=request, timeout=1)
if not response.ok: return if not response.ok: return
print(response.text)
# Get the latest release
releases_json = json.loads(response.text)
# index version
latest_release = [0, 0]
for release_json in releases_json:
release = Release(release_json)
if release.version_int > version_current.version_int:
latest_release[0] = releases_json.index(release_json)
latest_release[1] = release.version_int
if latest_release == [0, 0]: return
release = Release(releases_json[latest_release[0]])
print(f"v{version_current.version} -> v{release.tag_name}")
if release.version_int > version_current.version_int:
# Update
print("Update")
print(release.zip_file_url)
check_for_update() check_for_update()

30
test.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,30 @@
prepare_job:
stage: prepare # This stage must run before the release stage
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
script:
- echo "EXTRA_DESCRIPTION=some message" >> variables.env # Generate the EXTRA_DESCRIPTION and TAG environment variables
- echo "TAG=v$(cat VERSION)" >> variables.env # and append to the variables.env file
artifacts:
reports:
dotenv: variables.env # Use artifacts:reports:dotenv to expose the variables to other jobs
release_job:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
needs:
- job: prepare_job
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never # Do not run this job when a tag is created manually
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Run this job when commits are pushed or merged to the default branch
script:
- echo "running release_job for $TAG"
release:
name: 'Release $TAG'
description: 'Created using the release-cli $EXTRA_DESCRIPTION' # $EXTRA_DESCRIPTION and the $TAG
tag_name: '$TAG' # variables must be defined elsewhere
ref: '$CI_COMMIT_SHA' # in the pipeline. For example, in the

0
tests/__init__.py Normal file
View File

17
tests/edit_source.py Normal file
View File

@@ -0,0 +1,17 @@
with open(file="coverage.xml") as file:
file_content = file.readlines()
for line in file_content:
try:
orig_line = file_content.index(line)
line = line.strip().split("<")[1].split(">")
if line[0] == "source":
print("FOUND")
file_content[orig_line] = "<source>src</source>\n"
except:
pass
with open(file="coverage.xml", mode="w") as file:
file.writelines(file_content)

View File

@@ -0,0 +1,14 @@
with open(file="coverage.xml") as file:
file_content = file.readlines()
line_rate = None
for line in file_content:
try:
line = line.strip().split("<")[1].split(">")[0].split(" ")
if line[0] == "package":
line_rate = "Code coverage: " + str(float(line[2].split("=\"")[1].split("\"")[0])*100) + "%"
except:
pass
print(line_rate)

View File

View File

@@ -0,0 +1,44 @@
import os
import shutil
from pathlib import Path
from src.scan_folder import recursive_scan_folder
TEST_FOLDER = '.test_folder'
TEST_IMAGES = os.path.join('tests', 'test_files')
TEST_TEMP_FOLDER = os.path.join(TEST_FOLDER, 'Temp')
TEST_IMAGE_FOLDER = os.path.join(TEST_FOLDER, 'Images')
def create_file(file):
Path(os.path.join(TEST_FOLDER, file)).touch()
def delete_folder():
shutil.rmtree(TEST_FOLDER, ignore_errors=True)
def create_folders():
delete_folder()
os.makedirs(os.path.join(TEST_FOLDER, '001', '001'))
os.makedirs(os.path.join(TEST_FOLDER, '002', '001'))
def copy_test_images():
delete_folder()
os.makedirs(TEST_TEMP_FOLDER)
os.makedirs(TEST_IMAGE_FOLDER)
shutil.copy(src=os.path.join(TEST_IMAGES, 'test_image_001.JPG'), dst=TEST_TEMP_FOLDER)
shutil.copy(src=os.path.join(TEST_IMAGES, 'test_image_002.JPG'), dst=TEST_TEMP_FOLDER)
def copy_images(brand:str, model:str):
delete_folder()
create_folders()
files = recursive_scan_folder(path=TEST_IMAGES)
for file in files:
file_name = file.split(os.sep)[2:][0]
file_name = file_name.split("_")
if file_name[0] == brand and file_name[1] == model:
shutil.copy(src=file, dst=TEST_FOLDER)

7
tests/start_tests.sh Executable file
View File

@@ -0,0 +1,7 @@
uv run pytest \
--no-header \
-rfp \
--junitxml=tests/coverage/report.xml \
tests/
exit $?

13
tests/start_tests_gitlab.sh Executable file
View File

@@ -0,0 +1,13 @@
apk add --update libmagic
pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -r develop_requirements.txt
pwd
sh tests/start_tests.sh
exit_code=$?
cp tests/coverage/coverage.xml ./coverage.xml
cp tests/coverage/report.xml ./report.xml
python3 tests/get_coverage_percent.py
exit $exit_code

24
tests/test_config.py Normal file
View File

@@ -0,0 +1,24 @@
import unittest
from src import config as config_module
@unittest.skip
class TestConfig(unittest.TestCase):
def test_config_class(self):
test_config = {
"src": "/src/src",
"dst": "/src/dst",
"language": "en"
}
config = config_module.Config(test_config)
assert config.src == "/src/src"
assert config.dst == "/src/dst"
assert config.language == "en"
def test_get_config(self):
config_module.config_file = "src/config.yml"
config = config_module.get_config()
assert config.src == "../app/Temp"
assert config.dst == "../app/Bilder"
assert config.language == "en"

23
tests/test_exif_data.py Normal file
View File

@@ -0,0 +1,23 @@
from src.exif_data import ExifData
import unittest
class TestExifData(unittest.TestCase):
def test_exif_data(self):
exif_data_dict = {
"day": 2,
"month": 2,
"year": 2222,
"make": "CAMERA",
"time": "10:10:10",
"image_path": "/path/to/image",
"image_name": "Image.jpeg"
}
exif_data = ExifData(exif_data_dict)
# assert exif_data.make == "CAMERA"
assert exif_data.year == "2222"
assert exif_data.time == "10:10:10"
assert exif_data.day == "02"
assert exif_data.month == "02"
assert exif_data.year == "2222"

25
tests/test_file_handle.py Normal file
View File

@@ -0,0 +1,25 @@
import unittest
from unittest.mock import Mock
from helpers.folder_helper import TEST_FOLDER
from helpers.folder_helper import TEST_IMAGE_FOLDER
from helpers.folder_helper import delete_folder
from helpers.folder_helper import copy_test_images
from src.file_handler import sort_pictures
from src.scan_folder import recursive_scan_folder
from src.meta_data_handler import get_meta_data
class TestFileHandler(unittest.TestCase):
def test_file_handler(self):
copy_test_images()
files = recursive_scan_folder(TEST_FOLDER)
exif_data = get_meta_data(files)
sort_pictures(images=exif_data, logger=Mock(), dst=TEST_IMAGE_FOLDER)
sorted_pictures = recursive_scan_folder(TEST_FOLDER)
delete_folder()
assert len(sorted_pictures) == 2
assert "".join(picture for picture in sorted_pictures if '2023/10/25/test_image_002.JPG' in picture) != ""
assert "".join(picture for picture in sorted_pictures if '2023/10/28/test_image_001.JPG' in picture) != ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

View File

@@ -0,0 +1,20 @@
import unittest
import os
from src.meta_data_handler import get_meta_data
from src.meta_data_handler import get_image_meta_data
from src.scan_folder import recursive_scan_folder
TEST_IMAGES = "tests/test_files"
class TestMetadataHandler(unittest.TestCase):
def test_get_image_meta_data(self):
image_exif_data = get_image_meta_data(image_path=f"{TEST_IMAGES}/test_image_001.JPG")
assert image_exif_data.name == "test_image_001.JPG"
def test_get_meta_data(self):
images = recursive_scan_folder(TEST_IMAGES)
print(images)
exif_data = get_meta_data(images)
assert len(exif_data) == len(images)

24
tests/test_mime_types.py Normal file
View File

@@ -0,0 +1,24 @@
import unittest
from src.mime_types import MimeTypes
class TestMimeTypes(unittest.TestCase):
def test_mime_type_image(self):
mime_type = MimeTypes(file_path="tests/test_files/test_image_001.JPG")
assert mime_type.is_image
assert not mime_type.is_video
assert not mime_type.is_unsupported_file_type
def test_mime_type_video(self):
mime_type = MimeTypes(file_path="tests/test_files/test_video.mp4")
assert not mime_type.is_image
assert mime_type.is_video
assert not mime_type.is_unsupported_file_type
def test_mime_type_unsupported_file_type(self):
mime_type = MimeTypes(file_path="tests/test_mime_types.py")
assert not mime_type.is_image
assert not mime_type.is_video
assert mime_type.is_unsupported_file_type

14
tests/test_process.py Normal file
View File

@@ -0,0 +1,14 @@
import unittest
from unittest.mock import Mock
from src import config
from src.process import start_process
config.config_file = "src/config.yml"
class TestProcess(unittest.TestCase):
def test_process(self):
start_process(logger=Mock())

46
tests/test_scan_dir.py Normal file
View File

@@ -0,0 +1,46 @@
import unittest
from helpers.folder_helper import delete_folder
from helpers.folder_helper import create_folders
from helpers.folder_helper import create_file
from helpers.folder_helper import TEST_FOLDER
from src.scan_folder import scan_folder
from src.scan_folder import recursive_scan_folder
class TestScanFolder(unittest.TestCase):
def test_scan_folder(self):
create_folders()
create_file(file='img_01.jpeg')
files = scan_folder(TEST_FOLDER)
delete_folder()
assert len(files) == 1
def test_scan_empty_folder(self):
create_folders()
files = scan_folder(TEST_FOLDER)
delete_folder()
assert len(files) == 0
def test_scan_recursive_folder(self):
create_folders()
create_file(file='img_01.jpeg')
create_file(file='img_02.jpeg')
create_file(file='001/img_03.jpeg')
create_file(file='001/001/img_04.jpeg')
create_file(file='002/001/img_05.jpeg')
files = recursive_scan_folder(TEST_FOLDER)
delete_folder()
assert len(files) == 5
def test_scan_recursive_empty_folder(self):
create_folders()
files = recursive_scan_folder(TEST_FOLDER)
delete_folder()
assert len(files) == 0

View File

@@ -0,0 +1,52 @@
import unittest
from helpers.folder_helper import TEST_FOLDER
from helpers.folder_helper import copy_images
from src.meta_data_handler import get_meta_data
from src.scan_folder import recursive_scan_folder
class TestSamsung(unittest.TestCase):
def test_a54(self):
copy_images(brand="samsung", model="a54")
files = recursive_scan_folder(path=TEST_FOLDER)
meta_data = get_meta_data(images=files)
#for image in meta_data:
# assert image.make == "samsung"
image = next((image for image in meta_data if image.name == "samsung_a54_001.jpg"), None)
assert image.day == "02"
assert image.month == "12"
assert image.year == "2023"
image = next((image for image in meta_data if image.name == "samsung_a54_003.jpg"), None)
assert image.day == "08"
assert image.month == "12"
assert image.year == "2023"
@unittest.skip("")
def test_a52s(self):
copy_images(brand="samsung", model="a52s")
files = recursive_scan_folder(path=TEST_FOLDER)
meta_data = get_meta_data(images=files)
for image in meta_data:
assert image.make == "samsung"
@unittest.skip("")
def test_a14(self):
copy_images(brand="samsung", model="a14")
files = recursive_scan_folder(path=TEST_FOLDER)
meta_data = get_meta_data(images=files)
for image in meta_data:
assert image.make == "samsung"
@unittest.skip
class TestApple(unittest.TestCase):
def test_iphone_x(self):
copy_images(brand="iphone", model="x")
files = recursive_scan_folder(path=TEST_FOLDER)
meta_data = get_meta_data(images=files)
for image in meta_data:
assert image.make == "Apple"

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="0" time="0.024" timestamp="2024-03-17T01:13:57.781687" hostname="framework-13" /></testsuites>

275
uv.lock generated Normal file
View File

@@ -0,0 +1,275 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "autopicture-v3"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "filetype" },
{ name = "pillow" },
{ name = "progressbar" },
{ name = "pytest" },
{ name = "python-i18n", extra = ["yaml"] },
{ name = "python-magic" },
{ name = "pyyaml" },
{ name = "qifdp" },
{ name = "requests" },
]
[package.metadata]
requires-dist = [
{ name = "filetype", specifier = ">=1.2.0" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "progressbar", specifier = ">=2.5" },
{ name = "pytest", specifier = ">=9.0.2" },
{ name = "python-i18n", extras = ["yaml"], specifier = ">=0.3.9" },
{ name = "python-magic", specifier = ">=0.4.27" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "qifdp", url = "https://git.pinguin-software.de/DasMoorhuhn/qifdp/releases/download/25.12.9/qifdp-25.12.9-py3-none-any.whl" },
{ name = "requests", specifier = ">=2.32.4" },
]
[[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 = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[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 = "filetype"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" },
]
[[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.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[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 = "pillow"
version = "11.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
]
[[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 = "progressbar"
version = "2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/a6/b8e451f6cff1c99b4747a2f7235aa904d2d49e8e1464e0b798272aa84358/progressbar-2.5.tar.gz", hash = "sha256:5d81cb529da2e223b53962afd6c8ca0f05c6670e40309a7219eacc36af9b6c63", size = 10046, upload-time = "2018-06-29T02:32:00.222Z" }
[[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 = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "python-i18n"
version = "0.3.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/32/d9ba976458c9503ec22db4eb677a5d919edaecd73d893effeaa92a67b84b/python-i18n-0.3.9.tar.gz", hash = "sha256:df97f3d2364bf3a7ebfbd6cbefe8e45483468e52a9e30b909c6078f5f471e4e8", size = 11778, upload-time = "2020-08-26T14:31:27.512Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/73/9a0b2974dd9a3d50788d235f10c4d73c2efcd22926036309645fc2f0db0c/python_i18n-0.3.9-py3-none-any.whl", hash = "sha256:bda5b8d889ebd51973e22e53746417bd32783c9bd6780fd27cadbb733915651d", size = 13750, upload-time = "2020-08-26T14:31:26.266Z" },
]
[package.optional-dependencies]
yaml = [
{ name = "pyyaml" },
]
[[package]]
name = "python-magic"
version = "0.4.27"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
name = "qifdp"
version = "25.12.9"
source = { url = "https://git.pinguin-software.de/DasMoorhuhn/qifdp/releases/download/25.12.9/qifdp-25.12.9-py3-none-any.whl" }
wheels = [
{ url = "https://git.pinguin-software.de/DasMoorhuhn/qifdp/releases/download/25.12.9/qifdp-25.12.9-py3-none-any.whl", hash = "sha256:aa36c86d7d874c6ec1f1b537e6c330cb7e2da4206145e016ae83a1a12c804512" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]