Regroupement des fichiers de données avec PyInstaller (--onefile)


107

J'essaie de créer un fichier EXE unique avec PyInstaller qui doit inclure une image et une icône. Je ne peux pas pour la vie de moi faire fonctionner avec --onefile.

Si je le fais --onedir, tout fonctionne très bien. Lorsque j'utilise --onefile, il ne trouve pas les fichiers supplémentaires référencés (lors de l'exécution de l'EXE compilé). Il trouve les DLL et tout le reste bien, mais pas les deux images.

J'ai regardé dans le temp-dir généré lors de l'exécution de EXE ( \Temp\_MEI95642\par exemple) et les fichiers sont bien là. Quand je dépose le EXE dans ce répertoire temporaire, il les trouve. Très déroutant.

C'est ce que j'ai ajouté au .specfichier

a.datas += [('images/icon.ico', 'D:\\[workspace]\\App\\src\\images\\icon.ico',  'DATA'),
('images/loaderani.gif','D:\\[workspace]\\App\\src\\images\\loaderani.gif','DATA')]     

Je devrais ajouter que j'ai également essayé de ne pas les mettre dans des sous-dossiers, cela n'a fait aucune différence.

Modifier: la nouvelle réponse a été marquée comme correcte en raison de la mise à jour de PyInstaller.


11
Merci beaucoup! la ligne ici ( a.datas += ...) m'a vraiment aidé à l'instant. la documentation pyinstaller parle de l'utilisation COLLECTmais cela ne parvient pas à mettre les fichiers dans le binaire lors de l'utilisation--onefile
Igor Serebryany

@IgorSerebryany: Appuyé! J'ai juste eu exactement le même problème.
Hubro

Mon .exe plante lorsque je clique sur la barre de menu si j'utilise
iacopo

1
Tenez compte du fait que le dossier temporaire disparaît lorsque l'exécution se termine, donc pour vérifier ce qu'il contient, mettez un listdir de sys._MEIPASS dans votre programme principal
hithwen

Y a-t-il aussi un moyen d'utiliser la syntaxe Tree () que je semble avoir vu autour de l'endroit?
fatuhoku

Réponses:


152

Les nouvelles versions de PyInstaller ne définissent plus la envvariable, donc l'excellente réponse de Shish ne fonctionnera pas. Maintenant, le chemin est défini comme sys._MEIPASS:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

10
J'utilise pyinstaller 2 avec python 2.7 et je n'ai pas d' _MEIPASS2envois, mais sys._MEIPASSfonctionne bien, donc +1. Je suggère:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec

2
+1. Merci pour ça. En essayant d'utiliser la variable d'environnement _MEIPASS2 depuis un certain temps maintenant, j'ai évidemment écrit mon code alors qu'il était encore la variable d'environnement, car je me souviens que cela fonctionnait. Tout d'un coup, ça s'est arrêté, quand j'ai recompilé récemment.
Favil Orbedios

8
Je recommanderais d'attraper le AttributeErrorplutôt que le plus général Exception. J'ai rencontré des problèmes où il me manquait l'importation sys et le chemin par défaut était silencieux os.path.abspath.
Aaron le

Vous pourrez peut-être m'aider à résoudre mon problème .
Phillip

1
L'utilisation du répertoire de travail actuel dans le cas où _MEIPASS n'est pas défini est erronée. Voyez ma réponse .
Jonathon Reinhart le

51

pyinstaller décompresse vos données dans un dossier temporaire et stocke ce chemin de répertoire dans la _MEIPASS2variable d'environnement. Pour obtenir le _MEIPASS2répertoire en mode compressé et utiliser le répertoire local en mode décompressé (développement), j'utilise ceci:

def resource_path(relative):
    return os.path.join(
        os.environ.get(
            "_MEIPASS2",
            os.path.abspath(".")
        ),
        relative
    )

Production:

# in development
>>> resource_path("app_icon.ico")
"/home/shish/src/my_app/app_icon.ico"

# in production
>>> resource_path("app_icon.ico")
"/tmp/_MEI34121/app_icon.ico"

10
Comment, quand et où utiliserait-on cela?
gseattle le

4
Vous devez utiliser ce script dans le fichier .py que vous essayez de compiler avec PyInstaller. Ne mettez pas cet extrait de code dans le fichier .spec, cela ne fonctionnera pas. Accédez à vos fichiers en remplaçant le chemin par lequel vous tapez normalement resource_path("file_to_be_accessed.mp3"). Méfiez-vous que vous devez utiliser max 'answer pour la version actuelle de PyInstaller.
Exeleration-G

1
Cette réponse est-elle spécifique à l'utilisation de l' --one-fileoption?
fatuhoku

Vous pourrez peut-être m'aider à résoudre mon problème .
Phillip

L'utilisation du répertoire de travail actuel dans le cas où _MEIPASS n'est pas défini est erronée. Voyez ma réponse .
Jonathon Reinhart le

30

Toutes les autres réponses utilisent le répertoire de travail courant dans le cas où l'application n'est pas PyInstalled (c'est sys._MEIPASS-à- dire n'est pas définie). C'est faux, car cela vous empêche d'exécuter votre application à partir d'un répertoire autre que celui où se trouve votre script.

Une meilleure solution:

import sys
import os

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Merci Jon, votre fonction m'a aidé aujourd'hui à résoudre un problème. J'ai juste une question. Pourquoi certains écrivent _MEIPASS2 au lieu de _MEIPASS? Quand j'ai essayé d'ajouter les 2 auparavant, cela ne fonctionnait pas.
Ahmed AbdelKhalek

Je pense que os.env['_MEIPASS2'](utiliser l'environnement du système d'exploitation) était une ancienne méthode pour obtenir le répertoire. AFAIK sys._MEIPASSest la seule méthode correcte maintenant.
Jonathon Reinhart

Bonjour, votre solution fonctionne correctement lorsqu'elle resource_path()est dans le script principal. Existe-t-il un moyen de le faire fonctionner lorsqu'il est écrit dans un module à la place? À partir de maintenant, il essaiera d'obtenir des ressources dans le dossier où se trouve le module, pas dans le dossier principal.
Guimoute

1
@Guimoute semble que le code de cette réponse fonctionne comme prévu: il localise les ressources dans le module qui les fournit car il les utilise __file__. Si vous voulez qu'un module puisse accéder à des ressources en dehors de sa propre portée, vous devrez implémenter que le code d'importation fournisse un chemin vers celles-ci pour que votre module puisse les utiliser, par exemple par le module fournissant un appel "set_resource_location ()" que le les appels d'importateurs - ne pensez pas que ce soit différent de la façon dont vous devriez travailler lorsque vous n'utilisez pas pyinstaller.
barny le

@barny Merci pour l'indice! Je vais essayer quelque chose dans ce sens.
Guimoute

14

Peut-être ai-je manqué une étape ou fait quelque chose de mal, mais les méthodes ci-dessus n'ont pas regroupé les fichiers de données avec PyInstaller dans un seul fichier exe. Permettez-moi de partager les étapes de ce que j'ai fait.

  1. étape: écrivez l'une des méthodes ci-dessus dans votre fichier py avec l'importation des modules sys et os. J'ai essayé les deux. Le dernier est:

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
  2. étape: Écrivez, pyi-makespec file.py , sur la console, pour créer un fichier file.spec.

  3. étape: Ouvrez, file.spec avec Notepad ++ pour ajouter les fichiers de données comme ci-dessous:

    a = Analysis(['C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.py'],
                 pathex=['C:\\Users\\TCK\\Desktop\\Projeler'],
                 binaries=[],
                 datas=[],
                 hiddenimports=[],
                 hookspath=[],
                 runtime_hooks=[],
                 excludes=[],
                 win_no_prefer_redirects=False,
                 win_private_assemblies=False,
                 cipher=block_cipher)
    #Add the file like the below example
    a.datas += [('Converter-GUI.ico', 'C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico', 'DATA')]
    pyz = PYZ(a.pure, a.zipped_data,
         cipher=block_cipher)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='Converter-GUI',
              debug=False,
              strip=False,
              upx=True,
              #Turn the console option False if you don't want to see the console while executing the program.
              console=False,
              #Add an icon to the program.
              icon='C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico')
    
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=False,
                   upx=True,
                   name='Converter-GUI')
  4. étape: j'ai suivi les étapes ci-dessus, puis j'ai enregistré le fichier de spécifications. Enfin ouvert la console et écrire, pyinstaller file.spec (dans mon cas, file = Converter-GUI).

Conclusion: il y a encore plus d'un fichier dans le dossier dist.

Remarque: j'utilise Python 3.5.

EDIT: Enfin, cela fonctionne avec la méthode de Jonathan Reinhart.

  1. étape: ajoutez les codes ci-dessous à votre fichier python avec l'importation de sys et os.

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
  2. étape: Appelez la fonction ci-dessus en ajoutant le chemin de votre fichier:

    image_path = resource_path("Converter-GUI.ico")
  3. étape: écrivez la variable ci-dessus qui appelle la fonction là où vos codes ont besoin du chemin. Dans mon cas c'est:

        self.window.iconbitmap(image_path)
  4. étape: Ouvrez la console dans le même répertoire de votre fichier python, écrivez les codes comme ci-dessous:

        pyinstaller --onefile your_file.py
  5. étape: Ouvrez le fichier .spec du fichier python et ajoutez le tableau a.datas et ajoutez l'icône à la classe exe, qui a été donnée ci-dessus avant l'édition à la 3ème étape.
  6. étape: enregistrez et quittez le fichier de chemin. Accédez à votre dossier qui comprend le fichier spec et py. Ouvrez à nouveau la fenêtre de la console et tapez la commande ci-dessous:

        pyinstaller your_file.spec

Après l'étape 6., votre fichier est prêt à être utilisé.


Merci @dilde! Non pas que le a.datastableau de l'étape 5 prenne des tuples de chaînes, voir pythonhosted.org/PyInstaller/spec-files.html#adding-data-files pour référence.
Ian Campbell

Désolé, je ne peux pas comprendre une partie de votre message aussi bien que je le souhaite à cause de mon anglais faible. Cette partie est "Pas que les a.datas ...." , Vouliez-vous écrire 'Notez que les a.datas ... " Y a-t-il quelque chose qui ne va pas avec ce que j'ai écrit sur l'ajout de chaînes au tableau a.datas?
dildeolupbiten

Oups! Il faut noter que ... oui, désolé pour la faute de frappe. Vos démarches ont très bien fonctionné pour moi :), je n'ai pas pu trouver ces informations dans leur documentation ou ailleurs.
Ian Campbell

8

Au lieu de réécrire tout mon code de chemin comme suggéré, j'ai changé le répertoire de travail:

if getattr(sys, 'frozen', False):
    os.chdir(sys._MEIPASS)

Ajoutez simplement ces deux lignes au début de votre code, vous pouvez laisser le reste tel quel.


2
Non. Les bonnes applications devraient rarement changer le répertoire de travail. Il existe de meilleures façons.
Jonathon Reinhart le

3

Légère modification de la réponse acceptée.

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)

    return os.path.join(os.path.abspath("."), relative_path)

L'utilisation du répertoire de travail actuel dans le cas où _MEIPASS n'est pas défini est erronée. Voyez ma réponse .
Jonathon Reinhart le

1

La plainte / question la plus courante que j'ai vue avec PyInstaller est "mon code ne trouve pas un fichier de données que j'ai définitivement inclus dans le bundle, où est-il?", Et il n'est pas facile de voir quel / où votre code recherche parce que le code extrait se trouve dans un emplacement temporaire et est supprimé à sa sortie. Ajoutez ce morceau de code pour voir ce qui est inclus dans votre fichier unique et où il se trouve, en utilisant @Jonathon Reinhart'sresource_path()

for root, dirs, files in os.walk(resource_path("")):
    print(root)
    for file in files:
        print( "  ",file)

0

J'ai trouvé les réponses existantes déroutantes et j'ai mis beaucoup de temps à déterminer où se trouvait le problème. Voici une compilation de tout ce que j'ai trouvé.

Lorsque j'exécute mon application, j'obtiens une erreur Failed to execute script foo(si foo.pyest le fichier principal). Pour résoudre ce problème, n'exécutez pas PyInstaller avec --noconsole(ou modifiez main.specpour changer console=False=> console=True). Avec cela, exécutez l'exécutable à partir d'une ligne de commande et vous verrez l'échec.

La première chose à vérifier est que vous empaquetez correctement vos fichiers supplémentaires. Vous devez ajouter des tuples comme ('x', 'x')si vous souhaitez que le dossier xsoit inclus.

Après le crash, ne cliquez pas sur OK. Si vous êtes sous Windows, vous pouvez utiliser Rechercher tout . Recherchez l'un de vos fichiers (par exemple sword.png). Vous devriez trouver le chemin temporaire où il a décompressé les fichiers (par exemple C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Vous pouvez parcourir ce répertoire et vous assurer qu'il contient tout. Si vous ne pouvez pas le trouver de cette façon, recherchez quelque chose comme main.exe.manifest(Windows) ou python35.dll(si vous utilisez Python 3.5).

Si le programme d'installation inclut tout, le prochain problème probable est les E / S de fichiers: votre code Python recherche dans le répertoire de l'exécutable, au lieu du répertoire temporaire, des fichiers.

Pour résoudre ce problème, toutes les réponses à cette question fonctionnent. Personnellement, j'ai trouvé un mélange de tous pour fonctionner: changer de répertoire conditionnellement en premier lieu dans votre fichier de point d'entrée principal, et tout le reste fonctionne tel quel:

if hasattr(sys, '_MEIPASS'): os.chdir(sys._MEIPASS)


1
Désolé. Mais changer de répertoire n'est jamais la bonne réponse. Si un utilisateur fournit un chemin d'accès à l'application, il s'attend à ce qu'il soit relatif au répertoire actuel. Si vous changez cela, ce sera faux.
Jonathon Reinhart

0

Si vous essayez toujours de placer des fichiers relatifs à votre exécutable plutôt que dans le répertoire temporaire, vous devez le copier vous-même. C'est ainsi que j'ai fini par y arriver.

https://stackoverflow.com/a/59415662/999943

Vous ajoutez une étape dans le fichier de spécifications qui effectue une copie du système de fichiers dans la variable DISTPATH.

J'espère que cela pourra aider.


0

Je m'occupe de ce problème depuis longtemps (enfin, très longtemps). J'ai recherché presque toutes les sources mais les choses n'entraient pas dans un schéma dans ma tête.

Enfin, je pense avoir compris les étapes exactes à suivre, je voulais partager.

Notez que ma réponse utilise des informations sur les réponses des autres à cette question.

Comment créer un exécutable autonome d'un projet python.

Supposons que nous ayons un dossier_projet et que l'arborescence des fichiers se présente comme suit:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv

Tout d'abord, disons que vous avez défini vos chemins d'accès sound/et vos img/dossiers en variables sound_diret img_dircomme suit:

img_dir = os.path.join(os.path.dirname(__file__), "img")
sound_dir = os.path.join(os.path.dirname(__file__), "sound")

Vous devez les modifier comme suit:

img_dir = resource_path("img")
sound_dir = resource_path("sound")

Où, resource_path()est défini en haut de votre script comme:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Activez l'environnement virtuel si vous utilisez un venv,

Installez pyinstaller si vous ne l' avez pas encore, par: pip3 install pyinstaller.

Exécuter: pyi-makespec --onefile main.pypour créer le fichier de spécification pour le processus de compilation et de construction.

Cela changera la hiérarchie des fichiers en:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv
    main.spec

Ouvert (avec un édior) main.spec:

En haut, insérez:

added_files = [

("sound", "sound"),
("img", "img")

]

Ensuite, changez la ligne de datas=[],endatas=added_files,

Pour les détails des opérations effectuées sur main.specvoir ici.

Courir pyinstaller --onefile main.spec

Et qui est tout, vous pouvez exécuter mainà project_folder/distpartir de n'importe où, sans avoir rien d' autre dans son dossier. Vous ne pouvez distribuer que ce mainfichier. C'est maintenant, un véritable autonome .


@JonathonReinhart si vous êtes toujours là, je voulais vous informer que j'ai utilisé votre fonction dans ma réponse.
muyustan

0

En utilisant l'excellente réponse de Max et de cet article sur l'ajout de fichiers de données supplémentaires comme des images ou du son et mes propres recherches / tests, j'ai compris ce que je pense être le moyen le plus simple d'ajouter de tels fichiers.

Si vous souhaitez voir un exemple en direct, mon référentiel est ici sur GitHub.

Remarque: ceci est pour la compilation à l'aide de la commande --onefileou -Favec pyinstaller.

Mon environnement est le suivant.


Résoudre le problème en 2 étapes

Pour résoudre le problème, nous devons spécifiquement indiquer à Pyinstaller que nous avons des fichiers supplémentaires qui doivent être «regroupés» avec l'application.

Nous devons également utiliser un chemin `` relatif '' pour que l'application puisse fonctionner correctement lorsqu'elle s'exécute en tant que script Python ou EXE gelé.

Cela étant dit, nous avons besoin d'une fonction qui nous permet d'avoir des chemins relatifs. En utilisant la fonction que Max Posté, nous pouvons facilement résoudre le chemin relatif.

def img_resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

Nous utiliserions la fonction ci-dessus comme celle-ci afin que l'icône de l'application s'affiche lorsque l'application s'exécute en tant que script OU EXE gelé.

icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)

L'étape suivante est que nous devons indiquer à Pyinstaller où trouver les fichiers supplémentaires lors de la compilation afin que lorsque l'application est exécutée, ils soient créés dans le répertoire temporaire.

Nous pouvons résoudre ce problème de deux manières, comme indiqué dans la documentation , mais je préfère personnellement gérer mon propre fichier .spec, c'est ainsi que nous allons le faire.

Tout d'abord, vous devez déjà disposer d'un fichier .spec. Dans mon cas, j'ai pu créer ce dont j'avais besoin en exécutant pyinstallerdes arguments supplémentaires, vous pouvez trouver des arguments supplémentaires ici . Pour cette raison, mon fichier de spécifications peut sembler un peu différent du vôtre, mais je le poste tout pour référence après avoir expliqué les éléments importants.

added_files est essentiellement une liste contenant des tuple, dans mon cas, je ne veux ajouter qu'une seule image, mais vous pouvez ajouter plusieurs ico, png ou jpg en utilisant('app/img/*.ico', 'app/img')Vous pouvez également créer un autre tuple comme ceciadded_files = [ (), (), ()]pour avoir plusieurs importations

La première partie du tuple définit quel fichier ou quel type de fichier vous souhaitez ajouter ainsi que où les trouver. Considérez cela comme CTRL + C

La deuxième partie du tuple indique à Pyinstaller de créer le chemin «app / img /» et de placer les fichiers dans ce répertoire RELATIF au répertoire temporaire créé lorsque vous exécutez le .exe. Considérez cela comme CTRL + V

Sousa = Analysis([main... , j'ai définidatas=added_files, à l'origine c'était le cas,datas=[]mais nous voulons que la liste des importations soit, eh bien, importée, donc nous passons dans nos importations personnalisées.

Vous n'avez pas besoin de le faire à moins que vous ne vouliez une icône spécifique pour l'EXE, au bas du fichier de spécifications, je dis à Pyinstaller de définir l'icône de mon application pour l'exe avec l'option icon='app\\img\\app_icon.ico'.

added_files = [
    ('app/img/app_icon.ico','app/img/')
]
a = Analysis(['main.py'],
             pathex=['D:\\Github Repos\\Processes-Killer\\Process Killer'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Process Killer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True , uac_admin=True, icon='app\\img\\app_icon.ico')

Compilation en EXE

Je suis très paresseux; Je n'aime pas taper plus que nécessaire. J'ai créé un fichier .bat sur lequel je peux simplement cliquer. Vous n'êtes pas obligé de le faire, ce code fonctionnera très bien dans un shell d'invite de commande sans cela.

Puisque le fichier .spec contient tous nos paramètres de compilation et arguments (aka options), nous devons simplement donner ce fichier .spec à Pyinstaller.

pyinstaller.exe "Process Killer.spec"

0

Une autre solution consiste à créer un hook d'exécution, qui copiera (ou déplacera) vos données (fichiers / dossiers) vers le répertoire dans lequel l'exécutable est stocké. Le hook est un simple fichier python qui peut presque tout faire, juste avant l'exécution de votre application. Pour le définir, vous devez utiliser l' --runtime-hook=my_hook.pyoption pyinstaller. Donc, dans le cas où vos données sont un dossier d' images , vous devez exécuter la commande:

pyinstaller.py --onefile -F --add-data=images;images --runtime-hook=cp_images_hook.py main.py

Le cp_images_hook.py pourrait être quelque chose comme ceci:

import sys
import os
import shutil

path = getattr(sys, '_MEIPASS', os.getcwd())

full_path = path+"\\images"
try:
    shutil.move(full_path, ".\\images")
except:
    print("Cannot create 'images' folder. Already exists.")

Avant chaque exécution, le dossier images est déplacé vers le répertoire courant (à partir du dossier _MEIPASS), de sorte que l'exécutable y aura toujours accès. De cette façon, il n'est pas nécessaire de modifier le code de votre projet.

Deuxième solution

Vous pouvez profiter du mécanisme de hook d'exécution et changer le répertoire courant, ce qui n'est pas une bonne pratique selon certains développeurs, mais cela fonctionne bien.

Le code de crochet se trouve ci-dessous:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.