Programme complet

This commit is contained in:
Etienne GILLE
2026-03-27 16:10:28 +01:00
commit e0bb5fe181
8 changed files with 587 additions and 0 deletions

224
.gitignore vendored Normal file
View File

@@ -0,0 +1,224 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
# Nuitka
*.build
*.dist
*.ps1
*.ini
liste_fichiers

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"ymotongpoo.licenser",
"ms-python.python"
]
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"licenser.author": "Angers Loire Métropole",
"licenser.license": "BSD3"
}

29
LICENSE Normal file
View File

@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2026, Angers Loire Métropole
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

5
compiler.cmd Normal file
View File

@@ -0,0 +1,5 @@
@rem Copyright 2026 Angers Loire Métropole. All rights reserved.
@rem Use of this source code is governed by a BSD-style
@rem license that can be found in the LICENSE file.
@echo off
nuitka .\liste_fichiers.py --standalone --enable-plugins=tk-inter --windows-console-mode=disable --windows-icon-from-ico=tk86t_TK.ico

318
liste_fichiers.py Normal file
View File

@@ -0,0 +1,318 @@
# Copyright 2026 Angers Loire Métropole. All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
import configparser
import logging
import marshal
import tkinter as tk
import tkinter.filedialog as filedialog
import tkinter.messagebox as messagebox
from argparse import ArgumentParser, Namespace
from pathlib import Path
from tkinter import ttk
PATH = "."
SAVE_FILE = ".\\liste_fichiers"
CONFIG_FILE = ".\\liste_fichiers.ini"
LOGFORMAT = "%(asctime)s %(name)s %(levelname)s : %(message)s"
logger = logging.getLogger(__name__)
def make_file_list(path: Path) -> list[str]:
file_list = []
for file in path.iterdir():
if not file.is_dir():
file_list.append(file.name)
return file_list
def save_file_list(
path: Path = Path(PATH).absolute(), save_file: Path = Path(SAVE_FILE).absolute(), mode: str | None = None
) -> int:
logger.debug("Saving File List")
file_list = make_file_list(path)
with open(save_file, "wb") as output:
marshal.dump(file_list, output)
logger.debug(f"file list for {path} saved in {save_file}")
length = len(file_list)
logger.debug(f"{length} file recorded in {path}")
if mode == "save":
print(f"Nombre de fichiers dans le dossier: {length}")
return length
def compare_file_list(
path: Path = Path(PATH).absolute(), file_list_file: Path = Path(SAVE_FILE).absolute(), mode: str | None = None
) -> tuple[bool, int, int]:
logger.debug("Comparing File List")
with open(file_list_file, "rb") as input_file:
saved_files = marshal.load(input_file)
file_list = make_file_list(path)
comparison = len(saved_files) == len(file_list) and sorted(saved_files) == sorted(file_list)
if mode == "compare":
if comparison:
print("La liste des fichiers sauvegardée correspond aux fichiers dans le dossier")
else:
print("La liste des fichiers sauvegardée ne correspond pas aux fichiers dans le dossier")
print(f"Nombre de fichiers dans la liste sauvegardée: {len(saved_files)}")
print(f"Nombre de fichiers dans le dossier: {len(file_list)}")
logger.debug(f"comparison={comparison}; len(saved_files)={len(saved_files)}; len(file_list)={len(file_list)}")
return comparison, len(saved_files), len(file_list)
def gui(
path: Path = Path(PATH).absolute(), save_file: Path = Path(SAVE_FILE).absolute(), mode: str | None = None
) -> None:
logger.debug("Selected GUI mode")
class App(ttk.Frame):
def __init__(self, master=None):
super().__init__(master, padding=10)
self.grid()
ttk.Label(master, text="Chemin à comparer").grid(column=0, row=0)
self.path_entry = ttk.Entry(master)
self.path_entry.config(width=60)
self.path_var = tk.StringVar()
self.path_var.set(str(path))
self.path_entry["textvariable"] = self.path_var
self.path_entry.grid(column=1, row=0)
path_dialog_btn = ttk.Button(master, text="...")
path_dialog_btn.bind("<ButtonRelease>", self.open_path_dialog)
path_dialog_btn.grid(column=2, row=0)
ttk.Label(master, text="Fichier de liste de fichiers").grid(column=0, row=1)
self.save_file_entry = ttk.Entry(master)
self.save_file_entry.config(width=60)
self.save_file_var = tk.StringVar()
self.save_file_var.set(str(save_file))
self.save_file_entry["textvariable"] = self.save_file_var
self.save_file_entry.grid(column=1, row=1)
save_dialog_btn = ttk.Button(master, text="...")
save_dialog_btn.bind("<ButtonRelease>", self.open_save_dialog)
save_dialog_btn.grid(column=2, row=1)
self.btns = ttk.Frame(master)
self.btns.grid(column=0, columnspan=3, row=2)
self.save_btn = ttk.Button(self.btns, text="Sauvegarder")
self.save_btn.bind("<ButtonRelease>", self.save_file_list)
self.save_btn.pack(side="left")
self.compare_btn = ttk.Button(self.btns, text="Comparer")
self.compare_btn.bind("<ButtonRelease>", self.compare_file_list)
self.compare_btn.pack(side="left")
self.list_btn = ttk.Button(self.btns, text="Voir la liste des fichiers")
self.list_btn.bind("<ButtonRelease>", self.list_files)
self.list_btn.pack(side="left")
def list_files(self, event):
class ListDialog(ttk.Frame):
def __init__(self, path: Path, saved_file: Path, master=None):
super().__init__(master, padding=10)
self.grid()
with open(saved_file, "rb") as input_file:
saved_file_list = sorted(marshal.load(input_file))
file_list = sorted(make_file_list(path))
fl_i = 0
sf_i = 0
while fl_i < len(file_list) and sf_i < len(saved_file_list):
if saved_file_list[sf_i] == file_list[fl_i]:
sf_i += 1
fl_i += 1
elif saved_file_list[sf_i] in file_list:
saved_file_list.insert(sf_i, "-")
sf_i += 2
elif file_list[fl_i] in saved_file_list:
file_list.insert(fl_i, "-")
fl_i += 2
elif file_list[fl_i] not in saved_file_list and saved_file_list[sf_i] not in file_list:
file_list.insert(fl_i, "-")
saved_file_list.insert(sf_i + 1, "-")
fl_i += 2
sf_i += 2
while len(file_list) < len(saved_file_list):
file_list.append("-")
while len(saved_file_list) < len(file_list):
saved_file_list.append("-")
self.scroll = ttk.Scrollbar(self, orient="vertical", command=self.OnVsb)
self.scroll.grid(column=2, row=1, sticky="ns")
ttk.Label(self, text="Liste sauvegardée").grid(column=0, row=0, pady=5)
self.sflist = tk.Listbox(self, yscrollcommand=self.scroll.set, height=25)
self.sflist.bind("<MouseWheel>", self.OnMouseWheel)
self.sflist.grid(column=0, row=1)
for item in saved_file_list:
self.sflist.insert("end", item)
ttk.Label(self, text="Liste actuelle").grid(column=1, row=0, pady=5)
self.fllist = tk.Listbox(self, yscrollcommand=self.scroll.set, height=25)
self.fllist.bind("<MouseWheel>", self.OnMouseWheel)
self.fllist.grid(column=1, row=1)
for item in file_list:
self.fllist.insert("end", item)
btn = ttk.Button(self, text="Fermer", command=master.destroy)
btn.grid(columnspan=3, column=0, row=2)
def OnVsb(self, *args):
self.sflist.yview(*args)
self.fllist.yview(*args)
def OnMouseWheel(self, event):
self.sflist.yview("scroll", event.delta, "units")
self.fllist.yview("scroll", event.delta, "units")
return "break"
window = tk.Toplevel(self.master)
dlg = ListDialog(Path(self.path_var.get()).absolute(), Path(self.save_file_var.get()).absolute(), window)
dlg.mainloop()
def open_save_dialog(self, event):
filename = filedialog.asksaveasfilename(
initialfile=str(Path(self.save_file_var.get()).absolute()), filetypes=[("Tous les fichiers", "*")]
)
if filename:
self.save_file_var.set(filename)
def open_path_dialog(self, event):
foldername = filedialog.askdirectory(initialdir=self.path_var.get())
if foldername:
self.path_var.set(foldername)
def compare_file_list(self, event):
is_equal, saved_files, listed_files = compare_file_list(
Path(self.path_var.get()).absolute(), Path(self.save_file_var.get()).absolute()
)
if is_equal:
messagebox.showinfo(
"Comparaison",
f"La liste de fichiers sauvegardée et les fichiers dans le dossier correspondent.\nNombre de fichiers listés: {listed_files}",
)
else:
messagebox.showerror(
"Comparaison",
f"La liste de fichiers sauvegardée et les fichiers dans le dossiers ne correspondent pas.\nNombre de fichiers dans la sauvegarde: {saved_files}\nNombre de fichiers dans le dossier: {listed_files}",
)
def save_file_list(self, event):
saved_files = save_file_list(
Path(self.path_var.get()).absolute(), Path(self.save_file_var.get()).absolute()
)
messagebox.showinfo(
"Fichier sauvegardé",
f"Fichier {Path(self.save_file_var.get()).absolute()} sauvegardé.\nNombre de fichiers listés: {saved_files}",
)
root = tk.Tk()
root.title("Comparaison de liste de fichiers")
root.resizable(0, 0)
app = App(root)
app.mainloop()
def parser() -> Namespace:
arguments = ArgumentParser(
prog="liste_fichiers",
description="Outil de comparaison d'une liste de fichiers sauvegardée avec les fichiers actuellement présents dans un dossier",
)
arguments.add_argument(
"-p",
"--path",
help="répertoire à sauvegarder ou comparer",
)
arguments.add_argument(
"-f",
"--file_list",
dest="save_file",
help="fichier de sauvegarde de la liste de fichiers",
)
group = arguments.add_mutually_exclusive_group()
group.add_argument(
"-g",
"--gui",
action="store_const",
const="gui",
dest="mode",
default="gui",
help="utilisation du programme en mode interface graphique. Mode par défaut",
)
group.add_argument(
"-s",
"--save",
action="store_const",
const="save",
dest="mode",
help="sauvegarde de la liste de fichiers du répertoire",
)
group.add_argument(
"-c",
"--compare",
action="store_const",
const="compare",
dest="mode",
help="compare la liste de fichiers sauvegardée avec les fichiers actuels du répertoire",
)
arguments.add_argument(
"-d", "--debug", nargs="?", const=True, help="mode de déboggage, avec fichier de déboggage optionnel"
)
arguments.add_argument("-C", "--conf", help="fichier de configuration", default=CONFIG_FILE)
return arguments.parse_args()
def config(options: Namespace) -> Namespace:
print(options)
config_file = Path(options.conf).absolute()
if not config_file.exists():
config = configparser.ConfigParser()
config["liste_fichiers"] = {
"path": Path(options.path if options.path is not None else PATH).absolute(),
"save_file": Path(options.save_file if options.save_file is not None else SAVE_FILE).absolute(),
}
options.path = config["liste_fichiers"]["path"]
options.save_file = config["liste_fichiers"]["save_file"]
with open(config_file, "w") as cf:
config.write(cf)
else:
config = configparser.ConfigParser()
config.read(config_file)
if "liste_fichiers" in config:
if "path" in config["liste_fichiers"] and options.path is None:
options.path = config["liste_fichiers"]["path"]
else:
config["liste_fichiers"]["path"] = str(
Path(options.path if options.path is not None else PATH).absolute()
)
if "save_file" in config["liste_fichiers"] and options.save_file is None:
options.save_file = config["liste_fichiers"]["save_file"]
else:
config["liste_fichiers"]["save_file"] = str(
Path(options.save_file if options.save_file is not None else SAVE_FILE).absolute()
)
with open(config_file, "w") as cf:
config.write(cf)
else:
config["liste_fichiers"] = {
"path": Path(options.path if options.path is not None else PATH).absolute(),
"save_file": Path(options.save_file if options.save_file is not None else SAVE_FILE).absolute(),
}
with open(config_file, "w") as cf:
config.write(cf)
return options
if __name__ == "__main__":
options = parser()
if options.debug:
logger.setLevel(logging.DEBUG)
if type(options.debug) is str:
logging.basicConfig(filename=options.debug, encoding="utf-8", level=logging.DEBUG, format=LOGFORMAT)
else:
logging.basicConfig(format=LOGFORMAT, level=logging.DEBUG)
else:
logging.basicConfig(format=LOGFORMAT, level=logging.ERROR)
logger.debug("Mode de déboggage activé et configuré")
modes = {"gui": gui, "save": save_file_list, "compare": compare_file_list}
options = config(options)
modes[options.mode](Path(options.path).absolute(), Path(options.save_file).absolute(), options.mode)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
nuitka~=4.0.7

BIN
tk86t_TK.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB