Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0ca9cc1cc | |||
| cb45f67fff | |||
| 1486203dd2 | |||
| dad7a26544 | |||
| 5d3745a12c | |||
| 99a5c7c110 | |||
| c4fea238b3 | |||
| f4ace7efa3 | |||
| 0dea53d8f5 | |||
| c283e0221a | |||
| c051e120fe | |||
| cc3f0ea89b | |||
| 161525fbb5 | |||
| c21167f941 | |||
| 892df0144e | |||
| aaf7060252 | |||
| 0660628647 | |||
| e6c59507c4 | |||
| f8383567fe | |||
| a869b60933 | |||
| 3c8974ee74 | |||
| cdf8948193 | |||
| f2fd803b57 | |||
| 033eb269f0 | |||
| 480b97059d | |||
| e5ed2e7319 | |||
| 0f22b70e06 | |||
| ffdf3f3779 | |||
| 4d8c0c1dda | |||
| 6bea0ee524 | |||
| 5d7ca9172b | |||
| 0f6601ecd9 | |||
| f32822c861 | |||
| 52b776d495 | |||
| 941aa141af | |||
| 3e0a40eee9 | |||
| c774b8428c | |||
| d440f3ff1d | |||
| 6b4e8df4a5 | |||
| a5160aa0c9 | |||
| 250f7b6554 | |||
| d67b6f5669 | |||
| 968c549bbe | |||
| 0cf66c5781 | |||
| fc9ce09e0f | |||
| c6bda88974 | |||
| 4544681f45 | |||
| 27c5cd5076 | |||
| 0012c52adf | |||
| 9ae71af1aa | |||
| 3ebe5bba7f | |||
| 529776bac6 | |||
| d9ac0072ac | |||
| 2da422abc5 | |||
| cb9e02f4a2 |
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[run]
|
||||||
|
source = src
|
||||||
|
omit = tests/*, __init__.py, updater.py, main.py
|
||||||
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.idea/
|
.idea/
|
||||||
app/
|
app/
|
||||||
|
.test_folder/
|
||||||
|
*/coverage/
|
||||||
*.log
|
*.log
|
||||||
|
*.xml
|
||||||
|
.coverage
|
||||||
|
|
||||||
|
|||||||
13
.gitlab-ci.yml
Normal 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
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -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
|
||||||
|
|||||||
75
README.md
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-factoryboy
|
||||||
|
gitlabci-local
|
||||||
51
install.py
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
sudo apt-get install -y python3 python3-pip
|
||||||
11
install/AutoPictureV3.desktop
Executable 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
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
pythonpath = tests
|
||||||
|
addopts = --ignore-glob=**/tests/**
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
pillow
|
pillow
|
||||||
python-magic
|
pyyaml
|
||||||
|
python-magi
|
||||||
progressbar
|
progressbar
|
||||||
virtualenv
|
virtualenv
|
||||||
requests
|
requests
|
||||||
|
filetype
|
||||||
|
python-i18n[YAML]
|
||||||
|
pyinstaller
|
||||||
@@ -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
18
src/config.py
Normal 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
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
5
src/i18n_translations/de.yml
Normal 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'
|
||||||
5
src/i18n_translations/en.yml
Normal 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'
|
||||||
5
src/i18n_translations/fr.yml
Normal 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'
|
||||||
5
src/i18n_translations/it.yml
Normal 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'
|
||||||
5
src/i18n_translations/ru.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ru:
|
||||||
|
start_sorting_images: 'Начата сортировка %{image_count} изображения(ий)'
|
||||||
|
done_sorting_images: '%{image_count} изображение(ий) отсортировано за %{time} секунд'
|
||||||
|
done: 'Готово'
|
||||||
|
no_images_found: 'Изображения не найдены'
|
||||||
5
src/i18n_translations/uk.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
uk:
|
||||||
|
start_sorting_images: 'Розпочато сортування %{image_count} зображення(ь)'
|
||||||
|
done_sorting_images: '%{image_count} зображення(ь) відсортовано за %{time} секунд'
|
||||||
|
done: 'Готово'
|
||||||
|
no_images_found: 'Зображень не знайдено'
|
||||||
26
src/main.py
@@ -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")
|
|
||||||
|
|||||||
@@ -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"""
|
|
||||||
|
|
||||||
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):
|
|
||||||
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('video') != -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(".")
|
def is_file_picture(path:str):
|
||||||
# TODO: Sort out videos
|
mime = magic.Magic(mime=True)
|
||||||
img = Image.open(f"{image_path}")
|
file = mime.from_file(path)
|
||||||
|
if file.find('picture') != -1: return True
|
||||||
|
else: return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_raw(image:str):
|
||||||
|
# image_creation_time = os.path.getctime(filename=image)
|
||||||
|
# print(image_creation_time)
|
||||||
|
try:image_creation_time = get_raw_ifd_tag(file_path=image, read_buffer=0x100000)
|
||||||
|
except:return
|
||||||
|
date_time = image_creation_time.split(' ') # 'YYYY:MM:DD H:M:S'
|
||||||
|
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]
|
||||||
|
|
||||||
|
exif_data_dict = {
|
||||||
|
"day": day,
|
||||||
|
"month": month,
|
||||||
|
"year": year,
|
||||||
|
"time": time,
|
||||||
|
"image_path": image,
|
||||||
|
"image_name": image_name
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExifData(exif_data_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_image(image:str):
|
||||||
|
img = Image.open(image)
|
||||||
values = []
|
values = []
|
||||||
for tag, text in img.getexif().items():
|
for tag, text in img.getexif().items(): values.append([ExifTags.TAGS[tag], str(text)]) if tag in ExifTags.TAGS else {}
|
||||||
if tag in ExifTags.TAGS:
|
return filter_date_and_make(values, image_path=image)
|
||||||
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):
|
|
||||||
day = None
|
|
||||||
month = None
|
|
||||||
year = None
|
|
||||||
time = None
|
|
||||||
make = str(metaTags[1]).split("|")[1]
|
|
||||||
image_name = str(imagePath).split("/")[-1]
|
|
||||||
|
|
||||||
_date = str(metaTags[0]).split("|")
|
def handle_video(video:str):
|
||||||
time = _date[1].split(" ")[1]
|
pass
|
||||||
_date = _date[1].split(" ")[0].split(":")
|
|
||||||
day = _date[2]
|
|
||||||
month = _date[1]
|
|
||||||
year = _date[0]
|
|
||||||
|
|
||||||
return ExifData(image_path=imagePath, image_name=image_name, day=day, month=month, year=year, time=time, make=make)
|
|
||||||
|
|
||||||
def __filter_data(self, value):
|
def get_image_meta_data(image_path):
|
||||||
value_return = []
|
image_extension = str(image_path).split(os.sep)[-1].split('.')[1].upper()
|
||||||
for k in self.__keyWords:
|
# TODO: Sort out videos using mime type of file
|
||||||
for v in value:
|
# mime = MimeTypes(file_path=image_path)
|
||||||
temp = v.split("|")
|
|
||||||
if temp[0] == k:
|
|
||||||
value_return.append(v)
|
|
||||||
return value_return
|
|
||||||
|
|
||||||
def get_meta_data(self, images: list):
|
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 = []
|
exif_data_list = []
|
||||||
for image in images:
|
for image in images:
|
||||||
exif_data_list.append(self.__get_image_meta_data(image_path=image))
|
exif_data_list.append(get_image_meta_data(image_path=image))
|
||||||
return exif_data_list
|
return exif_data_list
|
||||||
|
|||||||
18
src/mime_types.py
Normal 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
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
17
tests/edit_source.py
Normal 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)
|
||||||
14
tests/get_coverage_percent.py
Normal 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)
|
||||||
0
tests/helpers/__init__.py
Normal file
44
tests/helpers/folder_helper.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) != ""
|
||||||
BIN
tests/test_files/iphone_x_001.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/iphone_x_002.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/iphone_x_003.jpeg
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
tests/test_files/iphone_x_004.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/test_files/iphone_x_005.jpeg
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
tests/test_files/iphone_x_006.jpeg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/test_files/samsung_a54_001.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
tests/test_files/samsung_a54_002.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_003.jpg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
tests/test_files/samsung_a54_004.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_005.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_006.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
tests/test_files/samsung_a54_007.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
tests/test_files/test_image_001.JPG
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/test_files/test_image_002.JPG
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/test_files/test_video.mp4
Normal file
20
tests/test_meta_data_handler.py
Normal 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
@@ -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
@@ -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
@@ -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
|
||||||
52
tests/test_with_real_data.py
Normal 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"
|
||||||
|
|
||||||
1
tests/tests/coverage/report.xml
Normal 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
@@ -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" },
|
||||||
|
]
|
||||||