Metadata-Version: 2.4
Name: automatheque.renommage
Version: 0.21.1
Summary: Renommage et rangement de fichiers par gabarits
Author-email: Marc <githubmarc@maj44.com>
License: LGPL-3.0-or-later
Project-URL: Home, https://github.com/jaegerbobomb/automatheque/tree/main/src/automatheque.renommage/
Project-URL: Repository, https://github.com/jaegerbobomb/automatheque.git
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: GPL-3.0.txt
Requires-Dist: automatheque>=0.19.0
Requires-Dist: attrs>=19.2
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# automatheque.renommage

Renommage et rangement de fichiers par gabarits.

## Détail

Un **gabarit** est un squelette de chemin — `{date:%Y}/{album}/{nom}` —
assorti d'une condition qui dit quand il s'applique et d'un ordre qui le
priorise. Le **renommeur** choisit le premier gabarit applicable, en déduit un
nouveau chemin, et y déplace le fichier.

C'est l'opération symétrique de `automatheque.decomposition` : là où celle-ci
tire des métadonnées d'un chemin, celle-ci construit un chemin à partir de
métadonnées. Les deux sont des distributions séparées parce que seule
celle-ci écrit sur le disque : un consommateur qui indexe sans jamais déplacer
n'a pas à en dépendre.

## Les briques

* **`Gabarit`** — un squelette, une condition, un ordre.
* **`Gabarits`** — la liste des gabarits, et l'algorithme de choix : d'abord
  ceux dont la condition est vérifiée, classés par ordre, puis ceux qui n'ont
  pas de condition. Un gabarit sans condition est donc un filet de sécurité,
  pas un concurrent.
* **`Renommable`** — mixin à faire hériter par l'objet à ranger. Il expose
  `filename` et surcharge `_gabarits_par_defaut()` et `_liste_champs_dispo()`.
* **`Renommeur`** — le déplacement lui-même.

## Exemple

```py
import attr

from automatheque.renommage import Gabarit, Gabarits, Renommable


@attr.s
class Photo(Renommable):
    album = attr.ib(default="", kw_only=True)
    annee = attr.ib(default="", kw_only=True)

    @classmethod
    def _gabarits_par_defaut(cls):
        return Gabarits(
            [
                Gabarit(
                    squelette="{annee}/{album}/{nom}", condition='"{album}"', ordre=1
                ),
                Gabarit(squelette="a-trier/{nom}", ordre=9),
            ]
        )

    def _liste_champs_dispo(self):
        return {"album": self.album, "annee": self.annee, "nom": ...}


photo = Photo(filename="/entree/DSC_0001.jpg", album="Japon", annee="2013")
photo.renomme("/photos")
# /photos/2013/Japon/DSC_0001.jpg
```

## La configuration est reçue, pas cherchée

Les gabarits vivent souvent dans un fichier de configuration :

```ini
[renommage]
r1 = ['{annee}/{album}/{nom}', '"{album}"', 1]
r2 = ['a-trier/{nom}', '', 9]
```

`Gabarits.depuis_configuration(config, section)` les en tire, et le résultat
est **passé** au renommeur :

```py
gabarits = Gabarits.depuis_configuration(charge_configuration(), "renommage")
Renommeur(photo, gabarits=gabarits).renomme("/photos")
```

Le renommeur ne consulte aucun état global : c'est l'appelant qui décide d'où
viennent ses gabarits. Le code d'origine appelait `charge_configuration()`
lui-même — une localisation de service, qui rendait le renommage dépendant
d'un fichier de configuration présent au bon endroit, et intestable sans lui.

## Ce que le renommage ne fait pas

* **Il n'écrit pas d'attributs étendus.** Le code d'origine posait
  discrètement `user.automatheque.fichier_orig` et
  `user.automatheque.modele.classe` dans les xattr du fichier, à chaque
  renommage. Les xattr ne survivent ni à la plupart des copies, ni aux
  archives, ni aux transferts réseau : c'est le plus fragile des supports pour
  de la provenance. Conserver le nom d'origine relève de l'application, qui
  sait où elle range ses métadonnées.
* **Il ne modifie pas le contenu du fichier.** Écrire des étiquettes dans une
  image est l'affaire d'un adaptateur.

## Transfert

Le déplacement est une copie, suivie d'une vérification de taille, suivie de
la suppression de l'original. `shutil.move` seul ne dirait pas si la copie
s'est mal passée d'un système de fichiers à l'autre ; ici, une cible qui ne
correspond pas lève `TransfertIncomplet`, **efface la cible douteuse** et
**laisse l'original en place**.

## Les champs ne peuvent pas sortir du répertoire cible

Les champs d'un squelette viennent des métadonnées des fichiers traités — un
album, une ville, un titre. Substitués tels quels, ils sortiraient du
répertoire demandé : `os.path.join` jette son premier argument dès que le
second est absolu, et `..` remonte d'un niveau.

Chaque champ **chaîne** est donc assaini avant substitution — séparateurs
neutralisés, segments `.` et `..` remplacés. Les valeurs non-chaînes passent
intactes, sans quoi `{date:%Y}` cesserait de fonctionner. Les séparateurs du
**squelette**, eux, sont conservés : c'est par eux que tu décris ton
arborescence.

En dernier recours, le chemin final est vérifié comme contenu dans le
répertoire cible ; sinon `CibleHorsRepertoire` est levée sans rien déplacer.
Un squelette **absolu** tombe donc sous cette garde.

## Requirement

Python >=3.9

## Installation

```bash
pip install automatheque.renommage
```

## License

LGPLv3.0 ou ultérieure
