dimanche 29 décembre 2024

Comment Générer en deux minutes une Librairie en Python : Guide Pratique et trivial

Si vous avez déjà cherché des informations sur la génération de librairies en Python, vous avez peut-être constaté que les ressources disponibles ne sont pas toujours claires ou faciles à suivre. C'est pourquoi j'ai décidé d'écrire cet article pour vous guider à travers les étapes de création et de génération d'une librairie Python de manière simple et efficace.

Structure de Projet

Pour commencer, supposons que votre projet ait la structure suivante :


mon_projet/
├── src/
│   └── pytools/
│       └── __init__.py
├── tests/
│   └── test_pytools.py
└── setup.py
    
  • src/ : Contient le code source de votre librairie.
  • tests/ : Contient les tests pour votre librairie.
  • setup.py : Le fichier de configuration pour générer le package.

Création du Fichier setup.py

Le fichier setup.py est essentiel pour définir les métadonnées de votre librairie et pour spécifier comment elle doit être construite et installée. Voici un exemple de ce à quoi il pourrait ressembler :


from setuptools import setup, find_packages

setup(
    name="pytools",
    version="0.1.0",
    packages=find_packages(where="src"),
    package_dir={"pytools":"src/pytools"},
    description="Une librairie privée",
    author="Yannick Beynet",
    python_requires=">=3.12",
)

    

Dans cet exemple :

  • name : Le nom de votre librairie.
  • version : La version actuelle de votre librairie.
  • packages : Utilise find_packages pour trouver les packages dans le dossier src.
  • package_dir : Spécifie que les packages sont dans le dossier src.
  • description : Une courte description de votre librairie.
  • author : Le nom de l'auteur.
  • python_requires : Indique la version minimum de Python requise.

Générer le Package

Une fois que votre fichier setup.py est en place, vous pouvez générer votre package en utilisant la commande suivante :

python -m build

Cette commande créera un fichier .whl (Wheel) et un fichier .tar.gz dans un dossier dist de votre projet. Ces fichiers sont les distributions de votre package, que vous pouvez ensuite distribuer et installer.

Le package généré se trouve dans le dossier dist. Vous pouvez l'installer en ligne de commande avec pip en utilisant la commande suivante :

pip install dist/pytools-0.1.0-py3-none-any.whl

Conclusion

Et voilà ! Vous avez maintenant un package Python prêt à être partagé. En suivant ces étapes simples, vous pouvez créer et distribuer vos propres librairies Python. N'oubliez pas de toujours tester votre code et de documenter vos fonctionnalités pour rendre votre librairie facile à utiliser par d'autres développeurs.

Si vous avez des questions ou des suggestions, n'hésitez pas à les laisser dans les commentaires ci-dessous. Bonne programmation !

dimanche 25 août 2019

Java FX WebView

N'ayant pas trouvé d'application light et simple me permettant de lire mes livres au format epub sur n'importe quel OS je me suis amusé à progammer une application java permettant de lire des fichiers epub ; pour cela j'ai un peu "joué" avec le composant JavaFX "WebView" . J'ai décidé de faire ce petit POST (le dernier date de longtemps) pour partager les problèmes que j'ai rencontré et les solutions que j'ai fini pas trouver en cherchant à gauche et à droite. En effet, si tout n'est pas pas trivial je n'ai pour l'instant pas été confronté à un problème insoluble.

Le b.a-ba : charger une page contenue dans une String

WebEngine engine = ebookView.getEngine();
Optional<String> page = ... // load the page in memory
engine.loadContent(page.get()); // load the page in the webview

Intercepter le click sur les liens

Enregistrer un EventListener sur les noeud DOM de notre choix

Les fichiers epub contiennent une table des matières permettant d'aller à une page particulière du livre, sous la forme d'une liste de liens. Ceux-ci sont par exemple sous la forme suivante :

<a href="pages/48297a6e74f19d19617e8a796ba1a73d.xhtml">page suivante</a>

Le problème est que le moteur du WebEngine ne sait pas résoudre ces liens ; c'est normal puisque ceux-ci sont des liens relatifs à la page courante qui est elle même contenue dans le fichier epub. Pour résoudre cette problématique j'ai cherché comment intercepter le click sur ce lien pour pouvoir ensuite aller chercher l'HTML attendu et le charger dans le webview. La solution m'a été inspirée de la javadoc du composant WebEngine et consiste à la fin du chargement de la page (State.SUCCEEDED) pour chaque balise "a" trouvée dans l'html à rajouter un listener (code java) qui sera automatiquement appelé par le moteur javascript du WebEngine. Le rajout de ce listener se fait à la fin du chargement de la page via le code suivant :

ebookView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
    @Override
    public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
        if (newState == Worker.State.SUCCEEDED) {
            EventListener listener = new EventListener() {
                @Override
                public void handleEvent(Event ev) {
                    if ("click".equals(ev.getType())) {
                        String href = ((Element)ev.getCurrentTarget()).getAttribute("href");
                        if (href==null) return;
                        Platform.runLater(()->currentEBook.ifPresent(e->ebookView.getEngine().loadContent(e.loadPage(href).orElse(""))));
                    }
                }
            };
            Document doc = ebookView.getEngine().getDocument();
            NodeList nodeList = doc.getElementsByTagName("a");
            for (int i = 0; i < nodeList.getLength(); i++) {
                ((EventTarget) nodeList.item(i)).addEventListener("click", listener, true);
            }
        }
    }
});

Ce code est intéressant car il montre que l'on peut manipuler l'arbre DOM de la page HTML courante : java offre une implementation de la spécification DOM.

Analysons plus en détail la ligne suivante :

((EvenChartTarget) nodeList.item(i)).addEventListener("click", listener, true);

En gros chaque noeud correspondant à une balise "a" est castée en EventTarget ce qui permet de lui associer l'EventListener "listener" créé quelques lignes au dessus. En théorie le cast doit être testé (voir spec DOM), mais dans notre cas nous savons que les balises "a" conviennent.

Problématique des balises filles de la balise "a"

Le troisième paramètre de la méthode addEventListener doit valoir "true". En effet, avant de passer ce paramètre à true j'avais certains liens pour lesquels la valeur de l'attribut href était toujours null au niveau de la ligne suivante :

String href = ((Element)ev.getCurrentTarget()).getAttribute("href");

Après avoir creusé, ce problème se posait pour des liens tels que celui là :

<a href="e9782919755387_fm01.html" class="toc_entry_frontMatter"><span class="b">Prologue</span></a>

Bien que l'EventListener soit enregistré sur la balise "a", par défaut c'est la balise fille "span" qui était retournée par la méthode getCurrentTarget et évidemment cette balise n'a pas d'attribut href - d'où la valeur null récupérée. Si l'on passe par contre le troisième paramètre à true on a le fonctionnement attendu : le listener est appelé avec la balise "a".

En fait il ne s'agit pas d'un bug, mais la spec DOM explique que l'événement est transmis à la balise la plus profonde ; sauf en passant ce paramètre à true. On aurait aussi pu aussi faire remonter (event bubbling) l'événement depuis la balise "span" vers la balise "a" : tout çà est décrit sur la page de spécification suivante pour ceux qui voudraient creuser : vous pouvez aller sur la specification du w3 ici.

Afficher des images "locales"

La solution est assez proche de celle utilisée pour les liens, elle consiste lorsque le document est chargé (Worker.State.SUCCEEDED) à remplacer la valeur de l'attribut src de la balise "img" par une URL que le moteur pourra résoudre. En l'occurrence une URL sous la forme suivante :

jar:file://<path to epub file>!<img path in the epub file>

Le code complet :

ebookView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) {
        if (newState == Worker.State.SUCCEEDED) {
            Document doc = ebookView.getEngine().getDocument();
            NodeList nodeList = doc.getElementsByTagName("img");
            for (int i = 0; i < nodeList.getLength(); i++) {
                Element img = (Element) nodeList.item(i);
                String href = img.getAttribute("src");
                                   // replace local url by global url in epub file
                currentEBook.map(e -> e.convertRessourceLocalPathToGlobalURL(href).orElse(null)).ifPresent(s->img.setAttribute("src",s));
            }
        }
    }
});

Autres Liens intéressants

  • Une page qui m'a permis de comprendre la structure des fichiers epub.
  • Javadoc JFX complète
  • Le résultat de mes petites expérimentations avec le WebView et les fichiers epub peut être chargé ici
    Comment l'utiliser :
    • View ebooks :
      java -jar <path to jar> --gui
    • To display informations about an ebook
      java -jar <path to jar> --info --target "<path to epub file>"
    • To add a category (Fantastic in this example) to ebook file
      java -jar <path to jar> --addSubject "Fantastic" --target "<path to epub file>"
    • To remove a category (Littérature in this example) from ebook file
      java -jar <path to jar> --removeSubject "Littérature" --target "<path to epub file>"

lundi 15 février 2016

Une alternative européenne à waze


Une application européenne

Un petit billet pour faire connaître icoyote, la dernière version mobile de coyote qui, à mes yeux, est une alternative crédible à waze. Si waze est une application géniale j'ai toujours un peu peur de l'utilisation qui peut être faite des données que je génère. L'application coyote est en effet payante, mais mes données n'ont pas vocation à être réutilisées par un quelconque moteur de recherche : les données collectées par icoyote sont entièrement anonymisées. Certes, l'application est un peu jeune, et souffre encore de quelques "bugs" mais elle s'améliore en continue.

Un système d'alertes réactif

Les alertes d'icoyote sont très faciles à déclarer et remontent très rapidement vers les autres utilisateurs ; la "communauté" coyote, bien que sans doute moins importante que celle des wazer, me semble après mes premières utilisations assez réactive et digne de confiance. Bref, c'est une application à essayer si vous voulez donner sa chance à un groupe plus proche de chez nous.
La cartographie d'icoyote, basée sur here, est assez claire et précise. Seul manque à ce jour : aucun moyen d'édition de la cartographie par la communauté.

dimanche 14 février 2016

La noyade du phénix


Le contexte

J'ai commencé à lire ce livre car il a été écrit par la fille d'un ami. Ma lecture a donc dans un premier temps été motivée par cette relation, histoire de soutenir une jeune auteure ; mais au fil des pages elle s'est transformée. J'ai commencé ce roman comme un simple curieux, amoureux de la lecture ; puis de plus en plus vite, j'ai été aspiré par l'histoire et comme l'héroïne j'ai été bousculé par les multiples rebondissements du récit. Finalement j'ai oublié qu'entre mes mains se trouvait l'oeuvre de quelqu'un de mon entourage pour ne plus y voir qu'une histoire passionnante qui chaque soir a su me transporter dans un univers différent du mien.

un roman prenant !

Il suffit donc de "tourner" quelques pages pour se sentir aspirer par l'histoire de l'héroïne, jeune romancière à la recherche de son passé familial. Avec une écriture imagée, à la fois réaliste et romantique, l'auteure - Audrey Briere - nous fait visiter les terres d'Ecosse. Ses personnages hauts en couleurs ne m'ont pas lâché pendant les quelques jours qu'il ma fallu pour tout lire. Suspens, amour, histoire, des ingrédients savamment mélangés dans "la noyade du phénix".

Silo

Sur ce blog j'ai toujours tendance à parler positivement des livres que je lis, le respect et l'admiration pour la condition d'écrivain étant très forts chez moi. Mais quand je dis que Silo est véritablement une découverte que j'ai fait, il y a deux mois, avec un grand plaisir. Ce n'est donc pas pour respecter les convenances que je vous invite à croire que Silo réunit de nombreuses qualités !

Roman d'anticipation

Tout d'abord il s'agit d'un bouquin d'anticipation ; l'action se passe en effet dans un futur post-apocalyptique, où l'humanité résiduelle survit dans un silo enchâssé dans le sol pour échapper à l'atmosphère désormais nocive, mortelle. Le "monde" qui est décrit dans cette oeuvre ne ressemble à aucun de ceux que j'ai pu lire auparavant.

Une société originale

Mais ce roman est bien plus qu'un énième livre de SF s'inspirant des sempiternelles "même ficelles" bien connues des amateurs du genre ; Silo est assez unique en son genre. L'auteur Hugh Howey nous dépeint une vision très intéressante des méthodes de survie mises en oeuvre par les survivants.
La société du silo est organisée en caste, chacune étant nécessaire à la survie générale. Pour maintenir la stabilité de ce microcosme des tabous ont été érigés et sont devenus lois ; les enfreindre peut conduire à une condamnation à mort. Les mises à mort elles même participent au bon fonctionnement de la communauté : le condamné doit sortir nettoyer les caméras du silo. Ces caméras sont les seuls fenêtres sur l'extérieur et permettent à ceux qui le veulent de voir dehors grâce à des projections sur un écran géant ; seul problème l'atmosphère souillée salie assez rapidement les caméras. Le condamné, banni du silo, doit donc les nettoyer rapidement, si possible avant que l'atmosphère ne le tue. Le spectacle, macabre, du nettoyage est au peuple du silo ce que le cirque était aux romains : un exutoire et un moyen de contrôle des foules. L'habilité de Hugh Howey tient dans la façon dont il décrit chacun des personnages et les luttes opposants les castes des plus démunies à celles des dirigeants. Chaque caractère est décrit en détail, avec sa part d'ombre et de lumière ; il n'y a pas seulement "les bons et les méchants" mais les intérêts des uns opposés à ceux des autres.

Pour conclure

Ce livre m'aura fait passer de bons moments. Je pense lire un peu plus tard les autres oeuvres de cet auteur, dont les suites de "silo" écrites suite au succès de ce dernier.

mercredi 29 avril 2015

Les listviews

L'objectif de ce billet est de faire un petit résumé riche en exemples des différentes notions que j'ai eu à utiliser en faisant joujou avec les ListView de javafx. J'ai commencé par une utilisation basique (affichage d'une bête liste statique) ; puis mes besoins sont allés crescendo. Cet article suivra la même chronologie que celle qui a été la mienne durant mon apprentissage.

Pour commencer

Tous les exemples de code seront récupérables sur ce dépôt git : git@github.com:beynet/listview.git
Pour exécuter le code démontré, importez le projet dans votre IDE (projet maven) ; la classe à exécuter est "ListViews".

Etape 1 - Une liste toute simple

Positionnement dans le code

Avec javafx, créer une listview et y rajouter/supprimer des éléments est très simple.
C'est ce que nous allons faire durant cette étape.
git checkout step1

Le code

Pour créer la liste :
ListView<Person> persons = new ListView<Person>();
Le code permettant de créer et ajouter des éléments à la liste est celui-ci (lignes 49 et 50 de la classe ListViews).
persons.getItems().add(new Person("John","Doe"));
persons.getItems().add(new Person("MyNameIs","Person"));
Ce qui donne :

Les personnes s'affichent correctement dans la liste car la classe Person surcharge la méthode toString() :
@Override
public String toString() {
    return getFirstName()+" "+getLastName();
};
Par défaut, notre listview a comme modèle de données une liste de type ObservableList<Person> ; les modifications apportées à cette liste (ajout et suppression) sont directement reportées dans la vue graphique. C'est ce qu'illustre notre bouton addPerson : il fait apparaitre une boite de dialogue permettant de rajouter une nouvelle personne (saisissez un prénom et un nom séparés d'un espace) :

Après appuie sur la touche ok la liste est mise à jour. Le code qui met la liste à jour est le suivant ( sur activation du bouton OK ) :
Button addPerson = new Button("aj. person");
addPerson.setOnAction(event -> {
    TextInputDialog askForName = new TextInputDialog();
    askForName.setContentText("new person name");
    Optional<string> result=askForName.showAndWait();
    result.ifPresent((name)->{
        String[] names=name.split(" ");
        if (names.length==2) {
            persons.getItems().add(new Person(names[0],names[1]));
        }
    });
});
Ce qui donne :

Etape 2 - Customiser l'affichage de la liste

Lors de cette étape nous allons voir comment customiser l'affichage des éléments de la liste.

Positionnement à l'étape 2

git checkout step2

Le code

Pour illustrer la customisation nous allons afficher le prénom en rouge et le nom en bleu.
Pour customiser l'affichage il faut implémenter une classe dérivant de ListCell. C'est notre classe PersonCell :
public class PersonCell extends ListCell<person> {
@Override
    protected void updateItem(Person item, boolean empty) {
        super.updateItem(item, empty);
        setText(null);
        setGraphic(null);
        if (item!=null && empty==false) {
            HBox content = new HBox();
            Label firstName = new Label(item.getFirstName()+" ");
            firstName.setTextFill(Color.RED);
            Label name = new Label(item.getLastName());
            name.setTextFill(Color.BLUE);
            content.getChildren().addAll(firstName,name);
            setGraphic(content);
        }
    }
}
Pour utiliser ces "cells" custom il faut associer un cell factory à la listview, c'est ce qui est fait en ligne 25 de la classe ListViews :
this.persons.setCellFactory(param -> {
   return new PersonCell();
});
Ce qui nous donne le résultat attendu :

Etape 3 - Rendre la listview réactive aux modifications des objets Person

Dans cette section nous allons montrer comment rendre la listview réactive aux modifications de son modèle de données sous-jacent ; c'est à dire comment modifier les éléments courant de la listview.

Récupération du code

git checkout step3

Le code

Pour cela nous allons modifier la classe Person pour lui rajouter la méthode permutFirstNameAndLastName(). Comme son nom l'indique cette méthode permettra de permuter le prénom et le nom d'une personne.
Comment informer la listview des modifications effectuées sur une personne ?
On peut imaginer supprimer de la liste la personne à modifier ; puis la rajouter une fois la modification effectuée. Cela marche, il suffit de faire quelque chose de ce style :

//suppression de la personne p1
persons.getItems().remove(p1);
p1.permutFirstNameAndLastName();
// rajout de p1 modifié
persons.getItems().add(p1);

Mais ce n'est pas la meilleur des solutions ; on peut facilement faire que la listview se modifie automatiquement quand son modèle de données est modifié. Pour cela nous allons utiliser les "propriétés" de javafx. Voir cet article (en anglais) pour tous les détails.

Modification de la classe Person

La classe Person est modifiée ainsi : les variables de classe firstName et lastName ne sont plus des instances de String mais des instances de StringProperty. En voici le code :
public class Person {
    public Person(String firstName, String lastName) {
        this.firstName = new SimpleStringProperty();
        this.firstName.set(firstName);
        this.lastName = new SimpleStringProperty();
        this.lastName.set(lastName);
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public String getFirstName() {
        return firstName.get();
    }

    public String getLastName() {
        return lastName.get();
    }

    @Override
    public String toString() {
        return getFirstName()+" "+getLastName();
    }


    /**
     * permut first and last name
     */
    public void permutFirstNameAndLastName() {
        String first = this.firstName.get();
        this.firstName.set(getLastName());
        this.lastName.set(first);
    }

    private StringProperty firstName;
    private StringProperty lastName;
}

Modification de la listview

La classe ListViews est modifiée pour rajouter un bouton permuttant le nom/prénom de la personne sélectionnée :
//this button will permute the selected person name and first name
Button permut = new Button("permut");
permut.setOnAction(event -> {
    Person selected = this.persons.getSelectionModel().getSelectedItem();
    if (selected!=null) {
        selected.permutFirstNameAndLastName();
    }
});
Enfin, la liste observable à laquelle la liste view sera couplée est construite "à la main" de façon à ce que la listview soit informée des changements sur les instances de Person que l'on veut propager. Le code tient en une seule ligne :
// associate the listview with a custom observable list
this.persons.setItems(FXCollections.observableArrayList(param -> new Observable[]{param.firstNameProperty(),param.lastNameProperty()} ) );
En résumé, pour chaque instance de personne on retourne un tableau d'observable qui contiendra la liste des propriétés à observer.

samedi 20 décembre 2014

La maîtresse de guerre

Un livre français

J'ai découvert Gabriel Katz avec la trilogie du puits des mémoires ; trilogie qui m'avait enthousiasmé. Quand j'ai vu qu'il sortait un nouveau livre je me suis précipité dessus et je n'ai pas été déçu.

L'histoire

Le personnage principale est une femme ,Kaelyn, fille d'un maître d'arme qui malgré son sexe rêve de reprendre la succession de son père. Elle va s'engager dans une guerre pour, d'après les recruteurs, combattre contre les esclavagistes d'un lointain pays. Convaincue d'être du bon côté Kaelyn s'engage donc ; mais la réalité de la guerre va très rapidement la rattraper. Ses certitudes seront vite chamboulées : les libérateurs s'avèrent brutaux, la culture du pays des esclavagistes paradoxalement plaisante.

Et alors ?

L'écriture de ce livre est limpide ; sa lecture semble couler de source. Si le style est plaisant, peut-être pourra-t-on reprocher au livre de manquer d'un peu de suspens : le scénario se déroule de manière très plaisante mais sans grandes surprises. Mais bon, je ne suis quand même pas resté sur ma faim ; Gabriel Katz a quand même accouché d'une belle oeuvre.

Ma note : 7/10