Python

logo python

Etiquettes: 

Python: Liste des paquets et des modules indispensables

Pour éviter tous problèmes lors des installations des modules externes pour Python, il est nécessaire d'installer les paquets suivants:

# apt-get install build-essential python-dev python3-dev python3-pip

# apt-get install libmysqlclient-dev gcc 

J'essaie de maintenir cette liste le plus à jour possible

Concernant les modules externes:

# pip install bpython httpie mysqlclient lxml requests virtualenv numpy bs4 pandas logger clipboard matplotlib pathlib pyftpdlib python-dateutil faker

 

Etiquettes: 

Python: Mettre à jour tous les paquets obsolètes

La commande pip list permet d'afficher la liste complète de tous les paquets python installés avec la version.

# pip list
asn1crypto (0.22.0)
backports.ssl-match-hostname (3.5.0.1)
batinfo (0.4.2)
blessings (1.6)
bottle (0.12.13)
bpython (0.16)
certifi (2017.4.17)
cffi (1.10.0)
chardet (3.0.4)
...
requests (2.18.1)
setuptools (36.2.0)
six (1.10.0)
statsd (3.2.1)
urllib3 (1.21.1)
wcwidth (0.1.7)
websocket-client (0.44.0)
zeroconf (0.19.1)

La commande suivante affiche les principales options disponibles pour la commande pip list:

# pip list -h
Usage:
  pip list [options]

Description:
  List installed packages, including editables.

  Packages are listed in a case-insensitive sorted order.

List Options:
  -o, --outdated              List outdated packages
  -u, --uptodate              List uptodate packages
  -e, --editable              List editable projects.
  -l, --local                 If in a virtualenv that has global access, do not list globally-installed packages.
  --user                      Only output packages installed in user-site.
  --pre                       Include pre-release and development versions. By default, pip only finds stable versions.
  --format <list_format>      Select the output format among: legacy (default), columns, freeze or json.
  --not-required              List packages that are not dependencies of installed packages.

Donc, pour obtenir la liste complète de tous les paquets Python obsolètes, il suffit d'utiliser l'option -o

# pip list -o
Package    Version Latest Type
---------- ------- ------ -----
decorator  4.0.11  4.1.1  wheel
setuptools 36.0.1  36.2.0 wheel

Ou alors directement avec la commande python et le module pip

# python3 -m pip list -o

La commande suivante va effectuer la mise à jour complète des paquets obsolètes.

# for x in $(pip list -o --format=columns | sed -n '3,$p' | cut -d' ' -f1); do pip install $x --upgrade; done
Collecting decorator
  Downloading decorator-4.1.1-py2.py3-none-any.whl
Installing collected packages: decorator
  Found existing installation: decorator 4.0.11
    Uninstalling decorator-4.0.11:
      Successfully uninstalled decorator-4.0.11
Successfully installed decorator-4.1.1
Collecting setuptools
  Downloading setuptools-36.2.0-py2.py3-none-any.whl (477kB)
    100% |████████████████████████████████| 481kB 1.1MB/s
Installing collected packages: setuptools
  Found existing installation: setuptools 36.0.1
    Uninstalling setuptools-36.0.1:
      Successfully uninstalled setuptools-36.0.1
Successfully installed setuptools-36.2.0

Attention à la version de pip utilisée.

En général, pip concerne Python2 et pip3 Python3

Une autre méthode:

$ python3 -m pip list -o > outdated
$ for x in $(sed -n '3,$p' outdated | awk '{print $1}'); do python3 -m pip install --upgrade --user $x; done

 

Python: Ajouter ou retrancher des mois à une date

A ma connaissance, en Python, à l'aide de la méthode timedelta du module datetime, il est très facile d'ajouter ou de retrancher des jours à une date mais quand il s'agit de le faire avec des mois, les choses se compliquent.
En effet tous les mois n'ayant pas le même nombre de jours, il devient incertain d'utiliser cette méthode.

J'ai donc développé cette fonction getNMonthsLessOrMore qui utilise à la fois le module datetime et le module calendar pour arriver au résultat souhaité.

from datetime import datetime
import calendar

def getNMonthsLessOrMore(startDate : datetime, nbMonths : int) -> list:

    class DateTimeFormat(Exception):
        pass

    class NbMonthsFormat(Exception):
        pass

    def getDayOfMonth(year, month, day):
        tmpCal = list(calendar.Calendar().itermonthdays(year, month))
        for x in range(day, 0, -1):
            if x in tmpCal: return x
        return 1

    try:
        if not isinstance(startDate, datetime): raise DateTimeFormat()
        if not isinstance(nbMonths, int): raise NbMonthsFormat()
    except DateTimeFormat:
        return False, "La date de départ doit être au format datetime.datetime"
    except NbMonthsFormat:
        return False, "Le nombre de mois doit être un entier positif ou négatif"
    else:
        if nbMonths == 0: return [startDate]
        L = []
        tmpDate = datetime(startDate.year, startDate.month, 1)
        if nbMonths < 0:
            for i in range(0, nbMonths, -1):
                y, m = calendar.prevmonth(tmpDate.year, tmpDate.month)
                tmpDate = datetime(y, m, getDayOfMonth(y, m, startDate.day))
                L.append(tmpDate)

        else:
            for i in range(0, nbMonths, 1):
                y, m = calendar.nextmonth(tmpDate.year, tmpDate.month)
                tmpDate = datetime(y, m, getDayOfMonth(y, m, startDate.day))
                L.append(tmpDate)
        return L

Exécution de la fonction:

>>> getNMonthsLessOrMore(datetime.now(), -6)
[datetime.datetime(2019, 12, 23, 0, 0), datetime.datetime(2019, 11, 23, 0, 0), datetime.datetime(2019, 10, 23, 0, 0), datetime.datetime(2019, 9, 23, 0, 0), datetime.datetime(2019, 8, 23, 0, 0), datetime.datetime(2019, 7, 23, 0, 0)]
>>> getNMonthsLessOrMore(datetime.now(), 8)
[datetime.datetime(2020, 2, 23, 0, 0), datetime.datetime(2020, 3, 23, 0, 0), datetime.datetime(2020, 4, 23, 0, 0), datetime.datetime(2020, 5, 23, 0, 0), datetime.datetime(2020, 6, 23, 0, 0), datetime.datetime(2020, 7, 23, 0, 0), datetime.datetime(2020, 8, 23, 0, 0), datetime.datetime(2020, 9, 23, 0, 0)]
>>> getNMonthsLessOrMore(datetime.now(), 0)
[datetime.datetime(2020, 1, 23, 7, 31, 43, 913244)]

Python: Ajouter/Mettre à jour facilement des données à un dictionnaire

En Python, l'objet dict est très utile pour indexer du contenu avec une clé.

Pour ajouter des données à un dictionnaire, la méthode est très simple.

>>> d = dict()
>>> d['a'] = 1
>>> d
{'a': 1}

Ajout de la valeur '1' au dictionnaire 'd' avec la lettre 'a' comme clé.

Pour mettre à jour la valeur de la clé 'a':

>>> d['a'] = 2
>>> d
{'a': 2}

Il est également possible d'indexer une liste dans un dictionnaire:

>>> d['l'] = list()
>>> d
{'a': 2, 'l': []}

et pour ajouter des données à ma liste:

>>> type(d['l'])
<class 'list'>
>>> d['l'].append(1)
>>> d
{'a': 2, 'l': [1]}

d['l'] étant une liste, je peux utliser la méthode append de la liste pour y ajouter des données

mais avant de pouvoir ajouter des données à une liste, il faut s'assurer que la clé existe, sinon une erreur est renvoyée

>>> d['ll'].append(1)
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d['ll'].append(1)
KeyError: 'll'

C'est pour cette raison que j'utilise la fonction suivante pour la gestion de mes dictionnaires.
Je ne me soucie plus de savoir si la clé existe ou pas.
Tout est controlé dans la fonction.

>>> def AddValueToDict(k, d, v, i):
    # k = key - d = dict - v = value - i = type value
    # si le dictionnaire 'd' contient la clé 'k'
    # on récupère la valeur
    if k in d: i = d[k]
    # détermination du type de la valeur
    # si la valeur est de type set()
    if   isinstance(i, set):   i.add(v)
    # si la valeur est de type list()
    elif isinstance(i, list):  i.append(v)
    # si la valeur est de type str()
    elif isinstance(i, str):   i += str(v)
    # si la valeur est de type int()
    elif isinstance(i, int):   i += int(v)
    # si la valeur est de type float()
    elif isinstance(i, float): i += float(v)
    # on met à jour l'objet 'i' pour la clé 'k' dans le dictionnaire 'd'
    d[k] = i
    # on retourne le dictionnaire 'd'
    return d

>>> d
{'a': 2, 'l': [1]}
>>> # Je veux ajouter au dictionnaire 'd'
>>> # la clé 'll' contenant la valeur '33' 
>>> # dans un objet de type list()
>>> d = AddValueToDict('ll', d, '33', list())
>>> d
{'a': 2, 'll': ['33'], 'l': [1]}
>>> # Ajout de la valeur 'aa' dans un objet de type list() pour la clé 'l'
>>> d = AddValueToDict('l', d, 'aa', list())
>>> d
{'a': 2, 'll': ['33'], 'l': [1, 'aa']}
>>> # Ajout de la valeur 'x' dans un objet de type set() pour la clé 's'
>>> d = AddValueToDict('s', d, 'x', set())
>>> d
{'a': 2, 'll': ['33'], 'l': [1, 'aa'], 's': {'x'}}
>>> # Ajout de la valeur 'x' dans un objet de type str() pour la clé 'a'
>>> d = AddValueToDict('a', d, ';x', str())
>>> d
{'a': 2, 'll': ['33'], 'l': [1, 'aa'], 's': {'x'}}
>>> d = AddValueToDict('a', d, 3, int())
>>> d
{'a': 5, 'll': ['33'], 'l': [1, 'aa'], 's': {'x'}}
>>> d = AddValueToDict('b', d, 'bb', str())
>>> d
{'a': 5, 'll': ['33'], 'b': 'bb', 'l': [1, 'aa'], 's': {'x'}}
>>> d = AddValueToDict('b', d, ';cc', str())
>>> d
{'a': 5, 'll': ['33'], 'b': 'bb;cc', 'l': [1, 'aa'], 's': {'x'}}

Dans le cas où la clé est inexistante dans le dictionnaire, elle est automatiquement créée avec la valeur 'v' dans un objet de type 'i'.
Si le type 'i' de la valeur 'v' est un set() ou une list(), la valeur est automatiquement ajoutée.
Si le type 'i' de la valeur 'v' est une chaine de texte str(), elle est concaténée à celle déjà existante.
Si le type 'i' de la valeur 'v' est un entier int() ou un float float(), elle est ajoutée à celle déjà existante.

Qu'en pensez-vous ?
Super simple !

Etiquettes: 

Python: Analyser des données avec Pandas et Matplotlib

Pandas et Matplotlib sont deux modules Python qui permettent d'analyser des données et de les représenter sous forme de graphiques.

Ce sont deux modules très complets et par conséquent très complexes.

Je vais présenter ici une analyse complète regroupant différentes fonctions utiles permettant de retourner le résultat souhaité.

Pour l'exemple, j'ai décidé d'analyser la consommation de carburant de ma Golf VII Hybride.

Mon analyse va porter sur les 16 dernières fois où je me suis rendu à la station service.

Voici les données brutes que je vais utiliser.
A l'origine mes données sont stockées dans un fichier XML.

<ope date="737309" amount="-29.449999999999999" category="185" wording="v=20.04 d=36773"/>

Ci-dessous une représentation sous forme de tableau.
Pour chaque date (ordinal), il y a le montant et une colonne contenant le volume d'essence acheté ainsi que le kilométrage du compteur au moment du plein.

 

  date amount wording
0 737309 -29.449999999999999 v=20.04 d=36773
1 737317 -43.170000000000002 v=30.02 d=37269
2 737333 -43.100000000000001 v=30.12 d=38061
3 737348 -44.240000000000002 v=31.31 d=38850
4 737358 -50.710000000000001 v=36.48 d=39632
5 737374 -50.149999999999999 v=34.16 d=40408
6 737387 -56.600000000000001 v=37.86 d=41032
7 737398 -37.369999999999997 v=25.49 d=41548
8 737403 -32.289999999999999 v=21.54 d=41880
9 737414 -49.340000000000003 v=32.57 d=42469
10 737427 -50.920000000000002 v=33.52 d=43104
11 737441 -53.57 v=34.90 d=43815
12 737455 -30.199999999999999 v=20.70 d=44353
13 737469 -39.240000000000002 v=27.08 d=44737
14 737484 -49.450000000000003 v=33.30 d=45403
15 737500 -29.850000000000001 v=22.98 d=45940

Voici donc mon script Python, complet, entièrement détaillé à l'aide des commentaires et le résultat final à la fin de cet article.

Ce script a été développé avec Python 3.7.5 et peut être exécuté avec JupyterLab

Je commence par importer les différents modules dont j'ai besoin.
Pour l'installation des modules nécessaires, tout est expliqué ici.

from pathlib import Path
import pandas as pd
from matplotlib import pyplot as plt
from collections import defaultdict
from datetime import datetime as dt
from bs4 import BeautifulSoup as bs
import numpy as np
from IPython.core.display import HTML
from scipy import stats
# La ligne ci-dessous permet d'afficher les graphiques dans jupyterlab
%matplotlib inline

Différentes fonctions qui vont m'être utiles pour transformer les données

def toDICT(KEYS, OPES, DICT):
    for OPE in OPES:
        for KEY in KEYS:
            DICT[KEY].append(OPE.get(KEY))
    return DICT

def toDF(DICT):
    # Je créé le dataframe Pandas à partir des données du dictionnaire DICT
    DF = pd.DataFrame(data=DICT)
    # Je converti la date (ordinal) au format datetime à l'aide de la fonction "convDATE"
    DF['date'] = DF['date'].apply(convDATE)
    # Je converti le montant en "float" et je récupère la valeur absolue
    DF['amount'] = DF['amount'].apply(float).apply(abs)
    # A l'aide de la fonction "getVolDist" je récupère le volume d'essence dans le texte de la colonne "wording"
    # Pour rappel, le texte contenu dans le champ "wording": v=25.49 d=41548
    # Le volume d'essence est converti en "float" à l'aide du module numpy
    DF['vol'] = DF['wording'].apply(getVolDist, args=['v']).apply(np.float32)
    # Le volume d'essence est arrondi à deux chiffres après la virgule
    DF['vol'] = DF['vol'].apply(lambda x: round(x, 2))
    # A l'aide de la fonction "getVolDist" je récupère le kilométrage dans le texte de la colonne "wording"
    # Le kilométrage est converti en "int" à l'aide du module numpy
    DF['dist'] = DF['wording'].apply(getVolDist, args=['d']).apply(np.int32)
    # Je supprime la colonne "wording"
    DF.drop(columns=['wording'], inplace=True)
    # Je trie mon dataframe par date croissante
    DF.sort_values('date', inplace=True)
    # A l'aide de la fonction "diff", appliquée sur la colonne "dist", je calcul la différence de kilométrage entre date et date+1
    DF['kms'] = DF['dist'].diff(periods=-1)
    # Je conserve la valeur absolue pour la colonne "kms"
    DF['kms'] = DF['kms'].apply(abs)
    # Je calcul le prix au litre arrondi à trois chiffres après la virgule
    DF['px/l'] = round(DF['amount'] / DF['vol'], 3)
    # Je calcul le cumul des kilomètres à l'aide la fonction "cumsum" appliquée sur la colonne "kms"
    DF['cum kms'] = DF['kms'].cumsum()
    # Je calcul le cumul du volume à l'aide la fonction "cumsum" appliquée sur la colonne "vol"
    DF['cum vol'] = DF['vol'].cumsum()
    # Je calcul la consommation arrondi à deux chiffres après la virgule
    DF['conso'] = round(DF['cum vol'] * 100 / DF['cum kms'], 2)
    # Je remplace toutes les valeurs nulles par le chiffre 0
    DF.fillna(value=0, inplace=True)
    return DF

def convDATE(i):
    return dt.fromordinal(int(i))

def getVolDist(s, k):
    s = s.split(' ')
    d = {x.split('=')[0]: x.split('=')[1] for x in s}
    return d.get(k)

Je récupère les donées au format XML et j'y applique les différentes transformation dont j'ai besoin.

# A l'aide du module Path, j'initialise mon fichier XML
FILE = Path('operations.xml')
# A l'aide du module BeautifulSoup, je parse mon fichier XML
XML = bs(FILE.read_text(), features='xml')
# Je recherche ensuite toutes les balises "ope" ayant un attribut "category" égal à 185
# Ceci est un exemple de mon fichier XML
# <ope date="737309" amount="-29.449999999999999" category="185" wording="v=20.04 d=36773"/>
# OPES est une liste qui contiendra toutes les balises recherchées
OPES = XML.findAll(name='ope', attrs={'category': '185'})
# J'initialise une liste avec le nom des colonnes que je souhaite avoir
KEYS = ['date','amount','vol','dist','wording']
# J'initialise un dictionnaire Python dont les valeurs seront par défaut des listes
DICT = defaultdict(list)
# J'exécute la fonction "toDict" qui va ajouter à mon dictionnaire "DICT" toutes les valeurs extraites dans le XML
DICT = toDICT(KEYS, OPES, DICT)
# J'exécute la fonction "toDF" pour créer un dataframe Pandas
DF = toDF(DICT)
# J'affiche le dataframe
HTML(DF.to_html())
  date amount vol dist kms px/l cum kms cum vol conso
0 2019-09-07 29.45 20.04 36773 496.0 1.470 496.0 20.04 4.04
1 2019-09-15 43.17 30.02 37269 792.0 1.438 1288.0 50.06 3.89
2 2019-10-01 43.10 30.12 38061 789.0 1.431 2077.0 80.18 3.86
3 2019-10-16 44.24 31.31 38850 782.0 1.413 2859.0 111.49 3.90
4 2019-10-26 50.71 36.48 39632 776.0 1.390 3635.0 147.97 4.07
5 2019-11-11 50.15 34.16 40408 624.0 1.468 4259.0 182.13 4.28
6 2019-11-24 56.60 37.86 41032 516.0 1.495 4775.0 219.99 4.61
7 2019-12-05 37.37 25.49 41548 332.0 1.466 5107.0 245.48 4.81
8 2019-12-10 32.29 21.54 41880 589.0 1.499 5696.0 267.02 4.69
9 2019-12-21 49.34 32.57 42469 635.0 1.515 6331.0 299.59 4.73
10 2020-01-03 50.92 33.52 43104 711.0 1.519 7042.0 333.11 4.73
11 2020-01-17 53.57 34.90 43815 538.0 1.535 7580.0 368.01 4.86
12 2020-01-31 30.20 20.70 44353 384.0 1.459 7964.0 388.71 4.88
13 2020-02-14 39.24 27.08 44737 666.0 1.449 8630.0 415.79 4.82
14 2020-02-29 49.45 33.30 45403 537.0 1.485 9167.0 449.09 4.90
15 2020-03-16 29.85 22.98 45940 0.0 1.299 0.0 472.07 0.00

Mes données sont enfin bien formatées.

Passons ensuite au graphique...

# J'initialise mon graphique
fig, axe = plt.subplots(figsize=(10.6,8))
# Je définis mon axe "x" avec les valeurs de la colonne "date" exceptée la dernière valeur
x = DF.iloc[:-1]['date'].dt.strftime('%Y-%m-%d')
# Je définis mon axe "y" avec les valeurs de la colonne "conso" exceptée la dernière valeur
y = DF.iloc[:-1]['conso']
# Je créé un graphique de type barres et de couleurs bleues 
axe.bar(x, y, label='Consommation', color='#1B80EA')
# J'ajoute un label à l'axe "y"
axe.set_ylabel('Consommation')
# J'ajoute un label à l'axe "x"
axe.set_xlabel('Période')
# Je définis la limite minimum de l'axe "y" en prenant en compte la valeur entière minimum de ma consommation
axe.set_ylim(int(DF.iloc[:-1]['conso'].min()))
# J'ajoute un titre au graphique en y indiquant la consommation moyenne calculée à l'aide de la fonction "mean" appliquée sur la colonne "conso" et arrondi à deux chiffres après la virgule 
axe.set_title('Golf VII: Conso moyenne: {} l/100kms'.format(round(DF.iloc[:-1].conso.mean(), 2)))
# Je formate automatiquement l'affichage des dates pour l'axe "x"
fig.autofmt_xdate()
# A partir d'ici, je créé un courbe de tendance sur la consommation
# Je définis un axe "x" temporaire contenant uniquement les valeurs de l'index. De 0 à 14
_x = DF.iloc[:-1].index
# A l'aide de la fonction "linregress" du module "scipy", je calcul la tendance de la consommation
lr = stats.linregress(_x, y)
# J'ajoute au graphique, sur les mêmes axes "x" & "y" la courbe de tendance de couleur rouge
axe.plot(x, lr.intercept + lr.slope * _x, marker='.', color='r', label='Tendance Consommation')
# Je positionne la légende dans le coin supérieur gauche
axe.legend(loc='upper left')
# Je créé un nouvel axe "y" 
ax2 = axe.twinx()
# Je définis mon nouvel axe "y" avec les valeurs de la colonne "px/l" exceptée la dernière valeur
y = DF.iloc[:-1]['px/l']
# Je crée un graphique de type ligne et de couleur or
ax2.plot(x, y, color='#C7A986', marker='o', label='px/l')
# J'ajoute un label au second axe "y"
ax2.set_ylabel('Prix au litre')
# Je positionne la légende en bas à droite
ax2.legend(loc='lower right')
# J'affiche le graphique
plt.show()

Je vais maintenant faire une synthèse de mes données.
Je souhaite avoir une représentation de ma consommation à chaque fin de mois.

# La fonction "resample" appliquée sur un dataframe permet de redéfinir les données
# Je souhaite regrouper mes données pour chaque fin de mois "M" appliqué sur la colonne "date
R = DF.resample('M',on='date')
# Je créé un nouveau dataframe à partir de ma synthèse et en agrégeant les données 
DF2 = R.agg({'amount':sum,'vol':sum,'dist':max,'kms':sum,'px/l':np.mean,'cum kms':max,'cum vol':max,'conso':np.mean})
# Je formate les différents nombres
DF2['conso'] = DF2['conso'].apply(lambda x: round(x, 2))
DF2['px/l'] = DF2['px/l'].apply(lambda x: round(x, 3))
DF2['kms'] = DF2['kms'].apply(int)
DF2['cum kms'] = DF2['cum kms'].apply(int)
# J'affiche le dataframe
HTML(DF2.to_html())
  amount vol dist kms px/l cum kms cum vol conso
date                
2019-09-30 72.62 50.06 37269 1288 1.454 1288 50.06 3.96
2019-10-31 138.05 97.91 39632 2347 1.411 3635 147.97 3.94
2019-11-30 106.75 72.02 41032 1140 1.482 4775 219.99 4.45
2019-12-31 119.00 79.60 42469 1556 1.493 6331 299.59 4.74
2020-01-31 134.69 89.12 44353 1633 1.504 7964 388.71 4.82
2020-02-29 88.69 60.38 45403 1203 1.467 9167 449.09 4.86
2020-03-31 29.85 22.98 45940 0 1.299 0 472.07 0.00

Intéressant comme synthèse.
Les dates de fin de mois ont été automatiquement calculées.

Et maintenant, le petit graphique qui va bien.

fig, axe = plt.subplots(figsize=(10.6,8))
x = DF2.iloc[:-1].index.strftime('%Y-%m-%d')
y = DF2.iloc[:-1]['conso']
axe.bar(x, y, label='conso', color='#1B80EA')
axe.set_ylabel('Consommation')
axe.set_xlabel('Période')
axe.set_ylim(int(y.min()))
_x = range(len(DF2.iloc[:-1].index))
lr = stats.linregress(_x, y)
axe.plot(x, lr.intercept + lr.slope * _x, marker='.', color='r', label='Tendance Consommation')
axe.legend(loc='upper left')
axe.set_title('Golf VII: Conso moyenne: {} l/100kms'.format(round(y.mean(), 2)))
fig.autofmt_xdate()
ax2 = axe.twinx()
y = DF2.iloc[:-1]['px/l']
ax2.plot(x, y, color='#C7A986', marker='o', label='px/l')
ax2.set_ylabel('Prix au litre')
ax2.legend(loc='lower right')
fig.savefig(FILE.parent / 'Analyse Conso Fin De Mois.png', format='png')
plt.show()

Vraiment bluffant Pandas et Matplotlib

Les dataframes Pandas offre tout un tas de possibilités qu'il est impossible de résumé en un seul article.

Les pages de documentation des projets sont très bien fournies.

Python: Arrêter un script proprement

J'utilise de temps en temps des scripts Python qui sont exécutés en tâche de fond sur mon système.

Le principe, une fonction main contenant le code à exécuter et une boucle while infinie qui exécute la fonction main.

#!python3
# -*- coding: UTF-8 -*-
import time

def main():
    ....

if __name__ == '__main__':
    while True:
        main()
        time.sleep(3)

Toutes les 3 sec, la fonction main est exécutée.

Le seul moyen d'arrêter le script, ctrl+c dans la console sans savoir où le script en est vraiement, au risque de l'arrêter pendant l'écriture de données dans un fichier et de corrompre ledit fichier.

Petite astuce, à mettre dans tous ses scripts exécutés en tâche de fond:

#!python3
# -*- coding: UTF-8 -*-
import time
from pathlib import Path
import sys

def main():
    ....

if __name__ == '__main__':
    while True:
        main()
        if Path('.kill').exists() and Path('.kill').read_text() == Path(sys.argv[0]).name:
            break
        time.sleep(3)
    sys.exit(0)

L'astuce consiste à utiliser un fichier .kill, dans le même répertoire que le script Python, et d'y écrire le nom exact du script concerné.

Python: Calculer la somme de contrôle d'un fichier

Calculer la somme MD5/SHA256/SHA512 de contrôle d'un fichier:

>>> import hashlib
>>> hashlib.md5(open('.bashrc','rb').read()).hexdigest()
'fc0db75cc50c25e8984fa2a958dac042'
>>> hashlib.sha256(open('.bashrc','rb').read()).hexdigest()
'2a684f93356c0cb229d6ee2e464d52e21f6dfbdafd3eef4d44d2f80d66bc20a1'
>>> hashlib.sha512(open('.bashrc','rb').read()).hexdigest()
'e5754975dad2512f024401d7f542a86b266dae7e301ff1f0a19c391e3494b1556544ed2de465da903c9978d7848e8e07fbcd32b03a2b47abbef96b80f4d30a52'
>>>

 

Python: Calculer le nombre PI

Le nombre π (3,1415.........), peut être calculé à l'aide de la formule suivante:

π = (4/1) - (4/3) + (4/5) - (4/7) + (4/9) - (4/11) + (4/13) etc etc ...

On démarre les opérations avec le numérateur "4", que l'on conserve jusqu'à la fin, et le dénominateur 1 que l'on incrémente de 2 à chaque opération en alternant soustraction et addition.

Plus le nombre d'itération est important, plus la précision sera importante.

Une petite boucle en Python permet de calculer π avec une bonne précision.

Dans l'exemple suivant, je vais boucler 100 millions de fois afin d'avoir une valeur de π avec 27 chiffres après la virgule, mais comme le souligne Matt, avec une précision de seulement 7 chiffres après la virgule, et cela en quelques minutes seulement.

>>> from decimal import Decimal
>>> P = 100000000 # attention, nombre pair obligatoire (voir commentaire)
>>> C = Decimal(4) / 1
>>> I = 1
>>> for i in range(P, 0, -1):
    z = Decimal(4) / (I+2)
    if i%2 == 0:
        z = Decimal.copy_negate(z)
    C += z
    I += 2

 >>> print(C)
3.141592663589793138462645118

Mais il est quand même plus rapide d'utiliser la fonction suivante:

>>> from math import pi
>>> print(pi)
3.141592653589793

C'était juste pour le fun

Etiquettes: 

Python: Calculer les classes ABC grâce à Pandas

Pandas propose une fonction qui permet de calculer très facilement les classes ABC.
Cette fonction se nomme cut

>>> import pandas as pd
>>> import numpy as np

Pour générer un dataframe avec des données aléatoires, voici une petite astuce:

>>> import random
>>> random.seed()
>>> df = pd.DataFrame(np.random.randint(0,1000,100), index=random.sample(range(100000, 999999), 100), columns=['stocks'])

Ca permet de générer un dataframe de 100 lignes avec une colonne "stocks" et contenant des valeurs comprises entre 0 et 1000 et en index, une liste d'articles uniques compris entre 100000 et 999999.

Exemple avec le dataframe suivant:

>>> df
             stocks
761920     965
636899     147
835336     324
366511     536
852098     544
        ...
170837     319
886380      98
676491     201
639583     233
854389     292

[100 rows x 1 columns]

En index, les références de mes articles et les quantités en stock dans la colonne 'stocks'.

J'ai au total 100 articles dans mon dataframe.

Premièrement, trier les données par stocks décroissant:

>>> df = df.sort_values('stocks', ascending=False)
>>> df
              stocks
506289     979
549641     977
351719     967
761920     965
874506     962
        ...
332012      67
797080      41
347642      38
417762      28
284562      13
[100 rows x 1 columns]

Ensuite, calculer le cumul des stocks:

>>> df['cumulStocks'] = df['stocks'].cumsum()
>>> df
              stocks  cumulStocks
506289     979          979
549641     977         1956
351719     967         2923
761920     965         3888
874506     962         4850
        ...          ...
332012      67        49118
797080      41        49159
347642      38        49197
417762      28        49225
284562      13        49238
[100 rows x 2 columns]

Après, il faut calculer le pourcentage du cumul du stock par rapport à la somme totale du stock:

>>> df['%cumulStocks'] = round(100 * df['cumulStocks'] / df['stocks'].sum(), 3)
>>> df
              stocks  cumulStocks  %cumulStocks
506289     979          979         1.988
549641     977         1956         3.973
351719     967         2923         5.936
761920     965         3888         7.896
874506     962         4850         9.850
        ...          ...           ...
332012      67        49118        99.756
797080      41        49159        99.840
347642      38        49197        99.917
417762      28        49225        99.974
284562      13        49238       100.000
[100 rows x 3 columns]

Nous ajoutons une colonne afin d'indiquer le rang des articles:

>>> df['rang'] = df.rank(ascending=False)
>>> df
               stocks  cumulStocks  %cumulStocks  rang
506289     979          979         1.988     1.0
549641     977         1956         3.973     2.0
351719     967         2923         5.936     3.0
761920     965         3888         7.896     4.0
874506     962         4850         9.850     5.0
        ...          ...           ...   ...
332012      67        49118        99.756    96.0
797080      41        49159        99.840    97.0
347642      38        49197        99.917    98.0
417762      28        49225        99.974    99.0
284562      13        49238       100.000   100.0
[100 rows x 4 columns]

Pour finir, calculons les classes ABC avec les pourcentages suivants:
Je veux que ma classe A concerne 80% des stocks cumulés, ma classe B les 15% suivants et ma classe C les 5 % restants.

>>> classes = ['A', 'B', 'C']
>>> pourcent = [0, 80, 95, 100]
>>> df['classe'] = pd.cut(df['%cumulStocks'], bins=pourcent, labels=classes)
>>> df
        stocks  cumulStocks  %cumulStocks  rang classe
506289     979          979         1.988     1.0      A
549641     977         1956         3.973     2.0      A
351719     967         2923         5.936     3.0      A
761920     965         3888         7.896     4.0      A
874506     962         4850         9.850     5.0      A
        ...          ...           ...   ...    ...
332012      67        49118        99.756    96.0      C
797080      41        49159        99.840    97.0      C
347642      38        49197        99.917    98.0      C
417762      28        49225        99.974    99.0      C
284562      13        49238       100.000   100.0      C
[100 rows x 5 columns]

Je me retrouve avec la répartition suivante, 56 articles en classe A, 22 articles en classe B et 22 articles en classe C

>>> df['classe'].value_counts(sort=False)
A    56
B    22
C    22
Name: classe, dtype: int64

La classe A contient bien tous les articles dont le pourcentage du stock cumulé est inférieur ou égal à 80%

>>> df[df['classe']=='A']['%cumulStocks'].describe()[['min','max']]
min       1.988000
max      79.776000
Name: %cumulStocks, dtype: float64

La classe B contient tous les articles supérieur à 80% et inférieur ou égal à 95%

>>> df[df['classe']=='B']['%cumulStocks'].describe()[['min','max']]
min      80.584000
max      94.549000
Name: %cumulStocks, dtype: float64

Enfin, la classe C contient tous les articles supérieur à 95% et inférieur ou égal à 100%

>>> df[df['classe']=='C']['%cumulStocks'].describe()[['min','max']]
min       95.022000
max      100.000000
Name: %cumulStocks, dtype: float64

Ajoutons une colonne indiquant le pourcentage du rang:

>>> df['%rang'] = round(100 * df['rang'] / len(df), 3)
>>> df
        stocks  cumulStocks  %cumulStocks  rang classe  %rang
506289     979          979         1.988     1.0      A    1.0
549641     977         1956         3.973     2.0      A    2.0
351719     967         2923         5.936     3.0      A    3.0
761920     965         3888         7.896     4.0      A    4.0
874506     962         4850         9.850     5.0      A    5.0
        ...          ...           ...   ...    ...    ...
332012      67        49118        99.756    96.0      C   96.0
797080      41        49159        99.840    97.0      C   97.0
347642      38        49197        99.917    98.0      C   98.0
417762      28        49225        99.974    99.0      C   99.0
284562      13        49238       100.000   100.0      C  100.0
[100 rows x 6 columns]

Vérifions maintenant si Pareto a toujours raison, à savoir 20% de mes articles doivent représenter 80% de mon stock (plus haut, la répartition indiquait 56 articles dans la classe A, par conséquent 56% puisque j'ai un échantillon de 100 articles):

>>> df[df['classe']=='A'].describe().loc['max',['%cumulStocks','%rang']]
%cumulStocks    79.776
%rang           56.000
Name: max, dtype: float64

On voit bien que 80% de mon stock est constitué par 56% de mes articles.

Sacré Pareto...

Python: Calculer les nombres premiers

class LowerThanTwo(Exception):
    pass

def isPrime(num):
    try:
        num = int(num)
        if num < 2: raise LowerThanTwo()
    except ValueError:
        return False, "Saisir un nombre valide !!!"
    except LowerThanTwo:
        return False, "Saisir un nombre supérieur égal à 2 !!!"
    else:
        if num > 2 and num % 2 == 0:
            return False, "{:>5d} n'est pas un nombre premier!".format(num)
        for x in range(2, num // 2):
            if num % x == 0:
                return False, "{:>5d} n'est pas un nombre premier!".format(num)
        return True, "{:>5d} est un nombre premier!".format(num)

Exécution de la fonction:

for x in range(100):
    a, b = isPrime(x)
    if a: print(b)

  2 est un nombre premier!
  3 est un nombre premier!
  5 est un nombre premier!
  7 est un nombre premier!
 11 est un nombre premier!
 13 est un nombre premier!
 17 est un nombre premier!
 19 est un nombre premier!
 23 est un nombre premier!
 29 est un nombre premier!
 31 est un nombre premier!
 37 est un nombre premier!
 41 est un nombre premier!
 43 est un nombre premier!
 47 est un nombre premier!
 53 est un nombre premier!
 59 est un nombre premier!
 61 est un nombre premier!
 67 est un nombre premier!
 71 est un nombre premier!
 73 est un nombre premier!
 79 est un nombre premier!
 83 est un nombre premier!
 89 est un nombre premier!
 97 est un nombre premier!

Un autre possibilité, consiste à utiliser une regex.

Astuce trouvée sur http://montreal.pm.org/tech/neil_kandalgaonkar.shtml?s=09

La négation de cette regex permet de savoir si le nombre est premier.

import re
r1 = re.compile(r'^1?$|^(11+?)\1+$')
not r1.match("1"*9) # False car 9 n'est pas un nombre premier
not r1.match("1"*13) # True car 13 est un nombre premier
not r1.match("1"*32002) # False car 32002 n'est pas un nombre premier
not r1.match("1"*32003) # True car 32003 est un nombre premier

Intéressant comme regex

Python: Choisir une date aléatoire ...

... entre le 01/01/1970 et aujourd'hui.

>>> import time
>>> import random
>>> time.strftime('%d/%m/%Y', time.gmtime(random.randint(0, int(time.time()))))
'20/09/1992'
>>> time.strftime('%d/%m/%Y', time.gmtime(random.randint(0, int(time.time()))))
'18/11/2007'

Ca peut toujours servir

Etiquettes: 

Python: Comment trouver dans une liste de nombres ceux dont la somme est égale à nombre défini

Comme indiqué dans le titre, j'ai une liste de nombre, par exemple 5, 10, 25, 30, 45, 60 et j'aimerais savoir quels sont ceux, quand on les additionne, qui me donne le résultat 35 par exemple.

Avec ces nombres, facile, il y a 10 + 25 mais également 5 + 30, mais quand on a une liste de nombres comme celle-ci 8.42, 6.94, 5.40, 1.77, 4.32, 4.26, 3.49, 2.33, 3.90, 1.09 (avec des décimales, c'est plus fun) et que je veux que la somme soit égale à 29.44, c'est un peu plus compliqué.

J'ai imaginé cette fonction en Python qui me permet de trouver le résultat.
Bien évidemment, plus il y a de nombres dans la liste de départ, plus le temps de calcul est long.
Pour m'aider, j'utilise les classes permutations et accumulate du module itertools ainsi que la classe Decimal du module decimal.

Peut-être auriez vous des idées d'amélioration ?

Voici la fonction.
Toutes les lignes sont commentées pour une meilleure compréhension.

from itertools import permutations
from itertools import accumulate
from decimal import Decimal


def trouve_les_arguments_de_la_somme():
    t = Decimal('29.44') # la somme à trouver
    l = [8.42, 6.94, 5.40, 1.77, 4.32, 4.26, 3.49, 2.33, 3.90, 1.09] # la liste des arguments, 10 au total
    l = [Decimal(str(x)) for x in l] # je convertis les arguments en objet "Decimal"
    l = [x for x in l if x <= t] # je conserve uniquement les valeurs inférieures ou égales à la somme à trouver
    z = permutations(l, len(l)) # j'utilise la classe permutations afin de générer toutes les combinaisons possible, 3 628 800 dans mon cas [factorial(10)]
    se = set() # je créé un set afin d'y stocker toutes les solutions possible
        for x in z: # je parcours toutes les combinaisons possible
            s = list(accumulate(x)) # pour chaque combinaison, je calcul la somme cumulée à l'aide de la classe accumulate
            if t in s: # si ma somme à trouver est dans la liste des sommes cumulées
                i = s.index(t) # je recherche la position de ma somme dans la liste
                se.add(tuple(sorted(x[:i + 1]))) # je récupère dans ma combinaison tous les arguments concernés grâce à mon index i et je les trie par ordre croissant avant de les ajouter à mon set
    for i in se:
            print(str(i)) # j'affiche les résultats

(Decimal('1.09'), Decimal('1.77'), Decimal('2.33'), Decimal('3.49'), Decimal('5.4'), Decimal('6.94'), Decimal('8.42'))

Dans mon cas, je n'ai qu'une seule solution possible (trouvée en 10 sec max).

Python: Comparaison de dossiers

Comparer le contenu de fichiers communs à deux dossiers (par exemple pour comparer les sources JAVA entre un environnement de DEV et un environnement de PROD) est relativement simple avec Python et le module Path.

La classe Path du module pathlib est indispensable pour tout ce qui touche au système de fichiers.
Il contient tout un tas d'outils très simple d'utilisation et qui facilite grandement toutes les opérations à faire.

J'utilise également la classe md5 du module hashlib afin de calculer la somme md5 de chaque fichier.

Import de la classe Path du module pathlib et de la classe md5 du module hashlib

>>> from pathlib import Path
>>> from hashlib import md5

Initialisation des deux dossiers à comparer.

>>> D1 = Path(r'd:\dossier1')
>>> D2 = Path(r'd:\dossier2')

A l'aide de la méthode rglob, je parcours récursivement tous les fichiers JAVA présents dans le dossier.
La méthode relative_to permet d'obtenir le chemin du fichier relatif à la racine (passée en paramètre).
La méthode exists permet de tester l'existence du fichier.
La méthode read_bytes permet d'accéder directement au contenu du fichier en mode binaire.
La méthode hexdigest de la classe md5 permet de calculer en hexadécimal la somme md5 du contenu du fichier.

>>> for file in D1.rglob('*.java'):
    file2 = D2/ file.relative_to(D1)
    if file2.exists():
        if md5(file.read_bytes()).hexdigest() != md5(file2.read_bytes()).hexdigest():
            print(f'Le fichier {file.relative_to(D1)} est différent entre {D1.as_posix()} et {D2.as_posix()}')
    else:
        print(f'Le fichier {file.relative_to(D1)} n\'existe pas dans {D2.as_posix()}')

infoUtilisation des f-strings pour l'affichage des messages.

Python: Comparer deux archives ZIP

Comparer deux fichiers archives ZIP afin d'ajouter dans le premier tous les fichiers présents dans le second sans prendre en compte les fichiers communs aux deux fichiers archives ZIP.

from pathlib import Path
from zipfile import ZipFile as zf

DOSSIER = Path('/mondossier')
filename1 = DOSSIER / 'archive1.zip'
# on ouvre la première archive en mode 'append'
zip1 = zf(filename1, mode='a')
# on fait un 'set' de tous les fichiers de la première archive
s1 = set([(x.filename, x.file_size) for x in zip1.filelist])
filename2 = DOSSIER / 'archive2.zip'
# on ouvre la seonde archive en mode 'read'
zip2 = zf(filename2, mode='r')
# on fait un 'set' de tous les fichiers de la seconde archive
s2 = set([(x.filename, x.file_size) for x in zip2.filelist])
# on parcourt tous les fichiers de la seconde archive qui ne sont pas dans la première archive
for f, s in s2 - s1:
    # on écrit dans la première archive le contenu du fichier de la seconde archive
    zip1.writestr(f, zip2.read(f))
zip1.close()
zip2.close()

On peut améliorer le script en vérifiant la taille du fichier (variable 's' dans la boucle 'for') à ajouter dans le cas où un fichier commun aurait été modifié entre les deux archives ZIP.

Python: Comparer le contenu de deux éléments

En Python, il existe un type d'éléments qui permet de faire différentes comparaisons.

Ce type, c'est le set.

Exemples avec les deux éléments suivants:

>>> s1 = {1,2,3,4,5}
>>> s2 = {3,4,5,6,7}

Afficher les éléments présents uniquement dans s1:

>>> s1 - s2
{1, 2}

Afficher les éléments présents uniquement dans s2:

>>> s2 - s1
{6, 7}

Afficher les éléments communs à s1 et à s2:

>>> s1 & s2
{3, 4, 5}

Afficher les éléments présents dans s1 et dans s2 (sans les doublons):

>>> s1 | s2
{1, 2, 3, 4, 5, 6, 7}

Vraiment très pratique pour comparer deux contenus.

Fonctionne également avec des chaines de textes.

Etiquettes: 

Python: Compter le nombre de jours ouvrés dans un mois

Pour calculer le nombre de jours ouvrés dans un mois, je vais utiliser les modules calendar et numpy

>>> import numpy as np
>>> import calendar

La méthode monthcalendar de la classe calendar permet d'obtenir un array avec tous les jours du mois classés par semaine (du lundi au dimanche)

>>> calendar.monthcalendar(2019,10)
[[0, 1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12, 13], [14, 15, 16, 17, 18, 19, 20], [21, 22, 23, 24, 25, 26, 27], [28, 29, 30, 31, 0, 0, 0]]

Je le convertis en array numpy

>>> a = np.array(calendar.monthcalendar(2019,10))
>>> a
array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 7,  8,  9, 10, 11, 12, 13],
       [14, 15, 16, 17, 18, 19, 20],
       [21, 22, 23, 24, 25, 26, 27],
       [28, 29, 30, 31,  0,  0,  0]])

J'affiche uniquement les 5 premiers jours de chaque semaine

>>> a[:,:-2]
array([[ 0,  1,  2,  3,  4],
       [ 7,  8,  9, 10, 11],
       [14, 15, 16, 17, 18],
       [21, 22, 23, 24, 25],
       [28, 29, 30, 31,  0]])

Enfin, je compte le nombre d'éléments supérieurs à 0

>>> len(a[np.where(a[:,:-2] > 0)])
23

Le mois d'octobre de l'année 2019 compte 23 jours ouvrés.

Il est également possible d'utiliser la méthode nonzero de la classe numpy

>>> len(a[np.nonzero(a[:,:5])])
23

Une autre méthode consiste à utiliser le puissant module dateutil

>>> from dateutil.parser import *
>>> from dateutil.rrule import *
>>> from dateutil.relativedelta import *
>>> dtstart = parse('20200601')
>>> len(list(rrule(DAILY, dtstart=dtstart, until=dtstart+relativedelta(months=+1, days=-1), byweekday=[MO, TU, WE, TH, FR])))
22

La classe parser permet de convertir une date d'un format texte à un format datetime.datetime
La classe relativedelta permet d'incrémenter une date ou de calculer l'écart entre deux dates
La classe rrule permet de générer toute une liste de dates suivant différents critères

Python: Connaitre le nom du système et le nom d'hôte de la machine

Exécuter un script Python sur plusieurs machines différentes, que ce soit au niveau du système d'exploitation et/ou de son nom d'hôte, peut engendrer des problèmes concernant certaines tâches à exécuter.

Par exemple, une variable initialisée différement en fonction du système d'exploitation et/ou du nom d'hôte de la machine sur laquelle le script est exécuté.

Les deux modules suivants permettent de résoudre ces problèmes.

Pour le nom du système d'exploitation:

>>> import platform
>>> print(platform.system())

Les différents retours en fonction du système:

  1. Linux > Linux
  2. Mac > Darwin
  3. Windows > Windows

Pour le nom d'hôte de la machine:

>>> import socket
>>> print(socket.gethostname())

 

Python: Convertir des secondes en heures, minutes et secondes

Pour convertir des secondes en heures, minutes et secondes, il existe une librairie Python, dateutil, qui permet de le faire tout simplement.

Pour ce faire, il suffit d'utiliser la méthode relativedelta de la classe dateutil.relativedelta.

Par exemple, pour convertir 16433.9 secondes:

>>> from dateutil.relativedelta import relativedelta
>>> relativedelta(seconds=16433.9)
relativedelta(hours=+4, minutes=+33, seconds=+53.9)
>>>

facile, non !

Python: Convertir un flux JSON en XML

Voici un script Python qui permet de convertir en XML un contenu JSON et/ou un dictionnaire de données Python.

linkToXml.py

Quelques exemples d'utilisations:

En ligne de commande (BASH par exemple) avec un pipe et un flux JSON via CURL:

# curl "http://api.geonames.org/citiesJSON?formatted=true&north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo&style=full" -o - -s | python3 ToXml.py
<?xml version="1.0" ?>
<XML>
        <geonames>
                <name>Mexiko-Stadt</name>
                <countrycode>MX</countrycode>
                <geonameId>3530597</geonameId>
                <toponymName>Mexico City</toponymName>
                <wikipedia>en.wikipedia.org/wiki/Mexico_City</wikipedia>
                <fclName>city, village,...</fclName>
                <fcode>PPLC</fcode>
                <lat>19.428472427036</lat>
                <lng>-99.12766456604</lng>
                <fcl>P</fcl>
                <fcodeName>capital of a political entity</fcodeName>
                <population>12294193</population>
        </geonames>
        <geonames>
                <name>Peking</name>
                <countrycode>CN</countrycode>
                <geonameId>1816670</geonameId>
                <toponymName>Beijing</toponymName>
                <wikipedia>en.wikipedia.org/wiki/Beijing</wikipedia>
                <fclName>city, village,...</fclName>
                <fcode>PPLC</fcode>
                <lat>39.9074977414405</lat>
                <lng>116.397228240967</lng>
                <fcl>P</fcl>
                <fcodeName>capital of a political entity</fcodeName>
                <population>11716620</population>
        </geonames>
</XML>

infoUtilisé avec un pipe, seul un flux JSON est autorisé.

Sinon, en important la classe dans un script Python:

A partir d'un fichier:

# cat flux.json
{
    "un": "one",
    "deux": "two",
    " 3": "three",
    "4": "four"
}

Code Python:

>>> from ToXml import ToXml
>>> toxml = ToXml()
>>> toxml.fromjson('flux.json')
>>> print(toxml.topretty())
<?xml version="1.0" ?>
<XML>
    <XMLNODE v="3">three</XMLNODE>
    <XMLNODE v="4">four</XMLNODE>
    <un>one</un>
    <deux>two</deux>
</XML>

>>>

A partir d'une chaine de texte représentant un contenu JSON:

>>> from ToXml import ToXml
>>> s = '{" 3": "three", "4": "four", "un": "one", "deux": "two"}'
>>> toxml = ToXml()
>>> toxml.fromjsons(s)
>>> print(toxml.topretty())
<?xml version="1.0" ?>
<XML>
    <XMLNODE v="3">three</XMLNODE>
    <XMLNODE v="4">four</XMLNODE>
    <un>one</un>
    <deux>two</deux>
</XML>

>>>

A partir d'un dictionnaire de données:

>>> from ToXml import ToXml
>>> d = {' 3': 'three', '4': 'four', 'un': 'one', 'deux': 'two'}
>>> type(d)
<class 'dict'>
>>> toxml = ToXml()
>>> toxml.fromdict(d)
>>> print(toxml.topretty())
<?xml version="1.0" ?>
<XML>
    <XMLNODE v="3">three</XMLNODE>
    <XMLNODE v="4">four</XMLNODE>
    <un>one</un>
    <deux>two</deux>
</XML>

>>>

Et enfin, une fonction permettant d'enregistrer le contenu XML dans un fichier:

>>> toxml.tofile('flux.xml')
# cat flux.xml
<?xml version="1.0" ?>
<XML>
    <XMLNODE v="3">three</XMLNODE>
    <XMLNODE v="4">four</XMLNODE>
    <un>one</un>
    <deux>two</deux>
</XML>

N'hésitez pas à laisser vos avis.

J'ai essayé de documenter le code au maximum mais au cas où ...

Etiquettes: 

Python: Créer un fichier CSV

#!/usr/bin/env python
# -*-coding: utf-8 -*

entetes = [
     u'Colonne1',
     u'Colonne2',
     u'Colonne3',
     u'Colonne4',
     u'Colonne5'
]

valeurs = [
     [u'Valeur1', u'Valeur2', u'Valeur3', u'Valeur4', u'Valeur5'],
     [u'Valeur6', u'Valeur7', u'Valeur8', u'Valeur9', u'Valeur10'],
     [u'Valeur11', u'Valeur12', u'Valeur13', u'Valeur14', u'Valeur15']
]

f = open('monFichier.csv', 'w')
ligneEntete = ";".join(entetes) + "\n"
f.write(ligneEntete)
for valeur in valeurs:
     ligne = ";".join(valeur) + "\n"
     f.write(ligne)

f.close()

attentionLa fonction open() de Python2 utilise l'encodage du système pour encoder le fichier (contrairement à Python3, il n'est pas possible de préciser l'encodage lors de louverture du fichier).
Pour encoder le fichier dans un encodage différent du système, il est nécessaire d'utiliser la fonction open() de la librairie codecs.

#!/usr/bin/env python
# -*-coding: utf-8 -*

import codecs

f = codecs.open('monFichier.csv', mode='w', encoding='iso-8859-1')
f.write(u'Ecriture réussie.')
f.close()

Ce qui donne:

$ file --mime-encoding monFichier.csv
monFichier.csv: iso-8859-1

link Liste des principaux encodages

Python: Créer un fichier XML à l'aide de BeautifulSoup

Le module BeautifulSoup permet de parser un fichier XML (ou HTML) très facilement mais il peut, tout aussi facilement, créer du contenu XML de toute pièce.

Pour l'exemple je vais utiliser le module faker qui permet de générer des données aléatoires en tout genre.

Installation des modules nécessaires

# python3 -m pip install --upgrade bs4 faker

Import des modules

>>> from bs4 import BeautifulSoup as bs
>>> from faker import Faker

Initialisation du module faker

>>> f = Faker('fr_FR')

Je vais générer des profils aléatoires ressemblant à ceci:

>>> f.profile()
{'job': 'aide-soignant',
 'company': 'Dias',
 'ssn': '318-53-8242',
 'residence': '74, rue Boyer\n04256 Hoarau',
 'current_location': (Decimal('17.293354'), Decimal('165.196114')),
 'blood_group': 'A+',
 'website': ['http://boulay.com/'],
 'username': 'arnaudmonique',
 'name': 'Patrick Valette',
 'sex': 'M',
 'address': '86, boulevard Raymond\n67114 Sainte Honoré',
 'mail': 'nbesnard@bouygtel.fr',
 'birthdate': datetime.date(1944, 10, 31)}

Les données générées sont au format json.
Je vais utiliser les clés comme noms des balises (tags) XML.
Mon fichier XML contiendra une balise (tag) principale "<profiles>" qui contiendra toutes les balises (tag) "<profile>".
La clé "ssn" sera un attribut de la balise (tag) "<profile>".
La valeur de la clé "current_location" sera découpée en deux balises (tags) XML, "<latitude>" et "<longitude>" et ces deux balises (tags) seront intégrées à la balise (tag) "<current_location>".
Toutes les valeurs de la clé "website" auront leurs propres balises (tags) XML "<website>" et elles seront toutes regroupées dans une balise (tag) XML "<websites>".

C'est parti...

Création de mon contenu XML de base

# Création de mon enveloppe XML
>>> xml = bs(features='xml')
# Création de mon tag <profiles>
>>> profiles = xml.new_tag('profiles')
# Ajout du tag <profiles> à l'enveloppe XML
>>> xml.append(profiles)

Création du détail de mon XML

# Une boucle pour créer 10 profils aléatoires
>>> for x in range(10):
    # Génération d'un profil aléatoire
    prof = f.profile()
    # Création de mon tag avec l'attribut "ssn" <profile ssn="xxx-xx-xxxx">
    profile = xml.new_tag('profile', attrs={'ssn':prof.get('ssn')})
    # Suppression de la clé "ssn" (plus besoin)
    prof.pop('ssn')
    # Je parcours toutes les clés de mon profil généré
    for k, c in prof.items():
        # Création du tag correspond à la clé du json <xxxx>
        y = xml.new_tag(k)
        # Si ma clé est "current_location" je découpe la valeur en "latitude" et "longitude"
        if k == 'current_location':
            # Création du tag <latitude>
            z1 = xml.new_tag('latitude')
            # Je renseigne la valeur du tag <latitude> avec la première valeur du tuple "current_location"
            z1.string = str(c[0])
            # Création du tag <longitude>
            z2 = xml.new_tag('longitude')
            # Je renseigne la valeur du tag <longitude> avec la seconde valeur du tuple "current_location"
            z2.string = str(c[1])
            # On ajoute les deux tags <latitude> et <longitude> au tag <current_location>
            y.append(z1)
            y.append(z2)
        # Si ma clé est "website" je détaille toutes les valeurs
        elif k == 'website':
            # je renomme mon tag en <websites>
            y = xml.new_tag('websites')
            # je parcours toutes les valeurs de la clé "website"
            for z in c:
                # Création du tag <website>
                z1 = xml.new_tag('website')
                # Je renseigne la valeur du tag <website>
                z1.string = z
                # J'ajoute mon tag <website> au tag <websites>
                y.append(z1)
        else:
            # Sinon, je renseigne la valeur du tag <xxxx>
            y.string = str(c)
        # J'ajoute le tag <xxxx> au tag <profile>
        profile.append(y)
    # J'ajoute le tag <profile> au tag <profiles>
    profiles.append(profile)
   

Et voilà, mon contenu XML est terminé.
Il ressemble à ceci:

<?xml version="1.0" encoding="utf-8"?>
<profiles>
    <profile ssn="002-43-2880">
        <job>projectionniste</job>
        <company>Rossi</company>
        <residence>rue Gomez
11458 Francoisnec</residence>
        <current_location>
            <latitude>-25.928603</latitude>
            <longitude>169.931343</longitude>
        </current_location>
        <blood_group>B+</blood_group>
        <websites>
            <website>https://www.bonnin.com/</website>
        </websites>
        <username>caronmargaret</username>
        <name>David Joubert</name>
        <sex>M</sex>
        <address>avenue Geneviève Giraud
90573 Martineaudan</address>
        <mail>aubertjacqueline@wanadoo.fr</mail>
        <birthdate>1968-02-29</birthdate>
    </profile>
    <profile ssn="007-06-6107">
        <job>prototypiste en matériaux souples</job>
        <company>Maurice Blin S.A.S.</company>
        <residence>798, rue Françoise Ramos
76458 Saint MargaudBourg</residence>
        <current_location>
            <latitude>41.5629005</latitude>
            <longitude>15.774012</longitude>
        </current_location>
        <blood_group>B+</blood_group>
        <websites>
            <website>https://fischer.com/</website>
            <website>http://daniel.net/</website>
            <website>https://paul.com/</website>
            <website>https://www.costa.com/</website>
        </websites>
        <username>marie50</username>
        <name>Inès Daniel</name>
        <sex>F</sex>
        <address>705, rue Thérèse Collet
50313 Jacques-les-Bains</address>
        <mail>hortensehuet@wanadoo.fr</mail>
        <birthdate>1968-06-15</birthdate>
    </profile>
    <profile ssn="017-32-3783">
        <job>hydraulicien</job>
        <company>Letellier et Fils</company>
        <residence>9, rue Gilles
61321 Sainte Virginie</residence>
        <current_location>
            <latitude>23.7306235</latitude>
            <longitude>-59.305275</longitude>
        </current_location>
        <blood_group>A+</blood_group>
        <websites>
            <website>https://www.chevallier.com/</website>
            <website>https://www.hoareau.net/</website>
        </websites>
        <username>rene93</username>
        <name>Céline Blin</name>
        <sex>F</sex>
        <address>43, avenue Vidal
82960 Teixeira</address>
        <mail>davidmartine@laposte.net</mail>
        <birthdate>1954-05-08</birthdate>
    </profile>
    <profile ssn="023-75-0442">
        <job>électricien installateur installatrice</job>
        <company>Dupuis</company>
        <residence>7, rue Jacob
27949 Saint Yves</residence>
        <current_location>
            <latitude>-36.2609615</latitude>
            <longitude>-40.304003</longitude>
        </current_location>
        <blood_group>O-</blood_group>
        <websites>
            <website>http://www.jean.com/</website>
            <website>https://gregoire.org/</website>
            <website>http://www.payet.org/</website>
            <website>http://www.dumas.org/</website>
        </websites>
        <username>wlegrand</username>
        <name>Thierry Martins</name>
        <sex>M</sex>
        <address>33, boulevard Henri Pierre
00436 Georges</address>
        <mail>dumashelene@wanadoo.fr</mail>
        <birthdate>1966-07-04</birthdate>
    </profile>
    <profile ssn="031-65-0389">
        <job>ingénieur logiciel</job>
        <company>Masse SA</company>
        <residence>boulevard de Perrin
96676 Ferrandnec</residence>
        <current_location>
            <latitude>-27.915336</latitude>
            <longitude>161.219956</longitude>
        </current_location>
        <blood_group>A+</blood_group>
        <websites>
            <website>https://www.rousseau.com/</website>
            <website>http://lecomte.com/</website>
            <website>http://lopez.fr/</website>
        </websites>
        <username>gabriel61</username>
        <name>Patrick Delattre</name>
        <sex>M</sex>
        <address>4, rue de Levy
46800 Hamon-sur-Laporte</address>
        <mail>aliceleveque@dbmail.com</mail>
        <birthdate>1943-08-04</birthdate>
    </profile>
    <profile ssn="042-20-4813">
        <job>télévendeur</job>
        <company>Peltier Lacombe S.A.R.L.</company>
        <residence>5, avenue Briand
68000 Coste</residence>
        <current_location>
            <latitude>-56.7088745</latitude>
            <longitude>149.494499</longitude>
        </current_location>
        <blood_group>O+</blood_group>
        <websites>
            <website>https://www.lelievre.fr/</website>
            <website>https://bernier.fr/</website>
        </websites>
        <username>noel32</username>
        <name>Luc Meyer</name>
        <sex>M</sex>
        <address>684, boulevard Margaret Garnier
96838 Da Costa</address>
        <mail>nicole82@bouygtel.fr</mail>
        <birthdate>1921-04-27</birthdate>
    </profile>
    <profile ssn="064-95-7931">
        <job>contremaître</job>
        <company>Roche</company>
        <residence>35, avenue de Andre
80266 Jacob-les-Bains</residence>
        <current_location>
            <latitude>-79.434306</latitude>
            <longitude>-50.008269</longitude>
        </current_location>
        <blood_group>O-</blood_group>
        <websites>
            <website>http://www.parent.fr/</website>
            <website>https://dufour.fr/</website>
            <website>http://www.humbert.com/</website>
            <website>https://www.boyer.net/</website>
        </websites>
        <username>ghernandez</username>
        <name>Gilbert Poirier</name>
        <sex>M</sex>
        <address>60, rue de Collet
40531 Morvanboeuf</address>
        <mail>clemence22@club-internet.fr</mail>
        <birthdate>1968-08-18</birthdate>
    </profile>
    <profile ssn="065-83-0793">
        <job>critique d'art</job>
        <company>Meunier</company>
        <residence>91, avenue Bertin
66624 Guillou</residence>
        <current_location>
            <latitude>40.394601</latitude>
            <longitude>28.203459</longitude>
        </current_location>
        <blood_group>AB+</blood_group>
        <websites>
            <website>http://www.pichon.com/</website>
            <website>http://chauveau.com/</website>
        </websites>
        <username>gillessabine</username>
        <name>Dominique Ferrand</name>
        <sex>F</sex>
        <address>rue Arnaude Baron
22300 Barbe</address>
        <mail>utorres@voila.fr</mail>
        <birthdate>1912-10-02</birthdate>
    </profile>
    <profile ssn="066-39-4549">
        <job>ingénieur de la police technique et scientifique</job>
        <company>Perret</company>
        <residence>96, chemin de Perret
91066 Perrier-la-Forêt</residence>
        <current_location>
            <latitude>-67.974417</latitude>
            <longitude>-32.491563</longitude>
        </current_location>
        <blood_group>O-</blood_group>
        <websites>
            <website>http://richard.net/</website>
            <website>http://www.fischer.fr/</website>
        </websites>
        <username>williamalbert</username>
        <name>Alex Godard</name>
        <sex>F</sex>
        <address>85, boulevard Guichard
20974 Marionnec</address>
        <mail>stephanieneveu@club-internet.fr</mail>
        <birthdate>1993-02-27</birthdate>
    </profile>
    <profile ssn="070-76-9294">
        <job>assistant en architecture</job>
        <company>Aubert Georges S.A.</company>
        <residence>31, rue de Garcia
41968 Saint Sabine</residence>
        <current_location>
            <latitude>36.045194</latitude>
            <longitude>131.046682</longitude>
        </current_location>
        <blood_group>O-</blood_group>
        <websites>
            <website>http://poirier.fr/</website>
        </websites>
        <username>denisalix</username>
        <name>Christophe Coste-Baron</name>
        <sex>M</sex>
        <address>44, boulevard Bertrand
85123 Blondel</address>
        <mail>bouvierremy@ifrance.com</mail>
        <birthdate>1908-03-12</birthdate>
    </profile>

Super simple quand même !

Python: Différentes manières d'initialiser un dictionnaire

Avec Python, il existe différentes possibilités pour initialiser un dictionnaire (dict()).

En voici trois.

La première:

>>> d = dict(un=1, deux=2, trois=3)
>>> d
{'un': 1, 'deux': 2, 'trois': 3}

La seconde, avec deux listes, une pour les clés et une pour les valeurs et la fonction zip:

>>> l1 = ['un','deux','trois']
>>> l2 = [1,2,3]
>>> d1 = dict(zip(l1, l2))
>>> d1
{'un': 1, 'deux': 2, 'trois': 3}

La troisième, toujours avec les deux listes et la fonction zip mais en utilisant, en plus, la compréhension de dictionnaire:

>>> l1 = ['un','deux','trois']
>>> l2 = [1,2,3]
>>> d2 = {k: v for k, v in zip(l1, l2)}
>>> d2
{'un': 1, 'deux': 2, 'trois': 3}

Avec la compréhension de dictionnaire, il est possible d'appliquer des traitements supplémentaires sur les clés et les valeurs.

Etiquettes: 

Python: Effectuer une recherche textuelle dans des fichiers textes

Il suffit de quelques lignes Python pour faire une recherche textuelle dans des fichiers textes.

A l'aide des deux modules pathlib et re, vous allez voir comme c'est simple.

Voici un exemple qui permet de rechercher une portion de code dans des fichiers JAVA.

>>> from pathlib import Path
>>> import re
>>> D = Path(r'C:\Users\ronan\javasrc')
>>> R = re.compile('public double .+;')
>>> for F in D.rglob('*.java'):
    if R.search(F.read_text()):
        print(F)

Et voilà, à chaque fois que la chaine recherchée est trouvée dans le contenu du fichier, son nom est affiché dans la console.

infoPour rappel, la fonction rglob de la classe Path du module pathlib permet d'effectuer une recherche récursive dans le dossier concerné.
La classe Path du module pathlib permet de lire le contenu des fichiers très simplement à l'aide de la fonction read_text.

Etiquettes: 

Python: Encoder / Décoder une URL

Pour encoder une URL (python2):

import urllib
url = "http://www.quennec.fr"
urlCodee = urllib.quote_plus(url)
print(urlCodee)
'http%3A%2F%2Fwww.quennec.fr'

Pour décoder une URL (python2):

import urllib
urlCodee = "http%3A%2F%2Fwww.quennec.fr"
url = urllib.unquote_plus(urlCodee)
print(url)
'http://www.quennec.fr'

Pour encoder une URL (python3):

Il faut utiliser le module urllib.parse (idem pour le décodage).

import urllib
url = "http://www.quennec.fr"
urlCodee = urllib.parse.quote_plus(url)
print(urlCodee)
'http%3A%2F%2Fwww.quennec.fr'

Python: Encoder un fichier en base64

Il est parfois nécessaire d'encoder en base64 des fichiers binaires tels que des images pour pouvoir les envoyer par mail par exemple.

import base64
with open("image.png", "rb") as image_file:
    encoded_string = base64.b64encode(image_file.read())

Le contenu de la variable encoded_string peut être envoyé dans un mail, inséré dans un champ d'une table d'une base de données etc etc ...

Python: Envoyer un mail tout simplement

Envoi d'un mail via un serveur SMTP (sans authentification):

import smtplib
from email.utils import formatdate
server = smtplib.SMTP()
# server.set_debuglevel(1) # Décommenter pour activer le debug
server.connect('smtp.toto.fr')
# (220, 'toto ESMTP Postfix') # Réponse du serveur
server.helo()
# (250, 'toto\nPIPELINING\nSIZE 10240000\nVRFY\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN') # Réponse du serveur
fromaddr = 'TOTO <moi@toto.fr>'
toaddrs = ['lui@toto.fr', 'elle@toto.fr'] # On peut mettre autant d'adresses que l'on souhaite
sujet = "Un Mail avec Python"
message = u"""\
Velit morbi ultrices magna integer.
Metus netus nascetur amet cum viverra ve cum.
Curae fusce condimentum interdum felis sit risus.
Proin class condimentum praesent hendrer
it donec odio facilisi sit.
Etiam massa tempus scelerisque curae habitasse vestibulum arcu metus iaculis hac.
"""
msg = """\
From: %s\r\n\
To: %s\r\n\
Subject: %s\r\n\
Date: %s\r\n\
\r\n\
%s
""" % (fromaddr, ", ".join(toaddrs), sujet, formatdate(localtime=True), message)
try:
    server.sendmail(fromaddr, toaddrs, msg)
except smtplib.SMTPException as e:
    print(e)
# {} # Réponse du serveur
server.quit()
# (221, '2.0.0 Bye') # Réponse du serveur

Envoi d'un mail via un serveur SMTP en SSL (avec authentification):

import smtplib
from email.utils import formatdate
server = smtplib.SMTP_SSL() # On utilise SMTP_SSL() à la place de SMTP()
# server.set_debuglevel(1) # Décommenter pour activer le debug
server.connect('smtp.toto.fr')
# (220, 'toto ESMTP Postfix') # Réponse du serveur
server.ehlo() # ATTENTION, avec SSL, c'est la commande EHLO au lieu de HELO
#(250, 'toto\nPIPELINING\nSIZE 10240000\nVRFY\nETRN\nAUTH LOGIN PLAIN\nAUTH=LOGIN PLAIN\nENHANCEDSTATUSCODES\n8BITMIME\nDSN') # Réponse du serveur
server.login('user', 'pass') # On s'authentifie
# (235, '2.7.0 Authentication successful') # Réponse du serveur
fromaddr = 'TOTO <moi@toto.fr>'
toaddrs = ['lui@toto.fr', 'elle@toto.fr'] # On peut mettre autant d'adresses que l'on souhaite
sujet = "Un Mail avec Python"
message = u"""\
Velit morbi ultrices magna integer.
Metus netus nascetur amet cum viverra ve cum.
Curae fusce condimentum interdum felis sit risus.
Proin class condimentum praesent hendrer
it donec odio facilisi sit.
Etiam massa tempus scelerisque curae habitasse vestibulum arcu metus iaculis hac.
"""
msg = """\
From: %s\r\n\
To: %s\r\n\
Subject: %s\r\n\
Date: %s\r\n\
\r\n\
%s
""" % (fromaddr, ", ".join(toaddrs), sujet, formatdate(localtime=True), message)
try:
    server.sendmail(fromaddr, toaddrs, msg)
except smtplib.SMTPException as e:
    print(e)
# {} # Réponse du serveur
server.quit()
# (221, '2.0.0 Bye')

Envoi d'un mail via un serveur SMTP en SSL + TLS (avec authentification):

import smtplib
from email.utils import formatdate
server = smtplib.SMTP('smtp.toto.fr', 587) # Avec TLS, on utilise SMTP()
# server.set_debuglevel(1) # Décommenter pour activer le debug
server.connect('smtp.toto.fr', 587) # On indique le port TLS
# (220, 'toto ESMTP Postfix') # Réponse du serveur
server.ehlo() # On utilise la commande EHLO
# (250, 'toto\nPIPELINING\nSIZE 10240000\nVRFY\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN') # Réponse du serveur
server.starttls() # On appelle la fonction STARTTLS
# (220, '2.0.0 Ready to start TLS') # Réponse du serveur
server.login('user', 'pass')
# (235, '2.7.0 Authentication successful') # Réponse du serveur
fromaddr = 'TOTO <moi@toto.fr>'
toaddrs = ['lui@toto.fr', 'elle@toto.fr'] # On peut mettre autant d'adresses que l'on souhaite
sujet = "Un Mail avec Python"
message = u"""\
Velit morbi ultrices magna integer.
Metus netus nascetur amet cum viverra ve cum.
Curae fusce condimentum interdum felis sit risus.
Proin class condimentum praesent hendrer
it donec odio facilisi sit.
Etiam massa tempus scelerisque curae habitasse vestibulum arcu metus iaculis hac.
"""
msg = """\
From: %s\r\n\
To: %s\r\n\
Subject: %s\r\n\
Date: %s\r\n\
\r\n\
%s
""" % (fromaddr, ", ".join(toaddrs), sujet, formatdate(localtime=True), message)
try:
    server.sendmail(fromaddr, toaddrs, msg)
except smtplib.SMTPException as e:
    print(e)
# {} # Réponse du serveur
server.quit()
# (221, '2.0.0 Bye')

infoEn cas d'erreur, lors de l'exécution de la commande server.sendmail(fromaddr, toaddrs, msg), le serveur répond en indiquant l'erreur rencontrée

{'elle@toto.fr': (450, '4.2.0 <elle@toto.fr>: Recipient address rejected: User unknown in relay recipient table')}

Et en HTML, c'est plus sympa

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate

server = smtplib.SMTP()
# server.set_debuglevel(1) # Décommenter pour activer le debug
server.connect('smtp.toto.fr')

server.helo()

fromaddr = 'TOTO <moi@toto.fr>'
toaddrs = ['lui@toto.fr', 'elle@toto.fr'] # On peut mettre autant d'adresses que l'on souhaite
sujet = "Un Mail avec Python"
html = u"""\
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div>
Velit morbi ultrices magna integer.
Metus netus nascetur amet cum viverra ve cum.
Curae fusce condimentum interdum felis sit risus.
Proin class condimentum praesent hendrer
it donec odio facilisi sit.
Etiam massa tempus scelerisque curae habitasse vestibulum arcu metus iaculis hac.
</div>
</body
</html>
"""

msg = MIMEMultipart('alternative')
msg['Subject'] = sujet
msg['From'] = fromaddr
msg['To'] = ','.join(toaddrs)
msg["Date"] = formatdate(localtime=True)

part = MIMEText(html, 'html')
msg.attach(part)

try:
    server.sendmail(fromaddr, toaddrs, msg.as_string())
except smtplib.SMTPException as e:
    print(e)

server.quit()

A la demande d'Alain, un exemple de script qui permet d'envoyer un mail avec une pièce jointe:
 

source:
https://docs.python.org/3/library/email.examples.html#email-examples
https://docs.python.org/3.8/library/mimetypes.html

from email.message import EmailMessage
from email.utils import formatdate
import mimetypes
from pathlib import Path
import smtplib
msg = EmailMessage()
msg['Subject'] = 'Un Mail avec Python'
msg['From'] = 'TOTO <moi@toto.fr>'
msg['To'] = ', '.join(['lui@toto.fr', 'elle@toto.fr'])
msg["Date"] = formatdate(localtime=True)
msg.set_content("""\
Salut!
Ci joint le fichier demandé.
""")
cfile = Path('myfile.txt')
ctype, encoding = mimetypes.guess_type(cfile)
if ctype is None or encoding is not None:
    ctype = 'application/octet-stream'
maintype, subtype = ctype.split('/', 1)
msg.add_attachment(cfile.read_bytes(), maintype=maintype, subtype=subtype, filename=cfile.name)
with smtplib.SMTP('smtp.toto.fr') as csmtp:
    csmtp.send_message(msg)

Python 3.9 - Envoi d'un mail via le SMTP office365 (outlook):

import smtplib
from email.utils import formatdate
server = smtplib.SMTP('SMTP.office365.com', 587)
server.connect('SMTP.office365.com', 587)
server.ehlo()
server.starttls()
server.login('julieraymond@example.org', 'Q7!7KgR3+5')
fromaddr = 'Antoine-Marcel Weber <julieraymond@example.org>'
toaddrs = ['renee88@example.org', 'etienneguyot@example.net']
sujet = "Reculer voile finir arrêter complètement lever."
message = u"""\
Extraordinaire longtemps taille musique. Discussion expérience français anglais fleur toujours.
Professeur dieu agir ouvrir escalier facile supposer. Action maintenir contenter bois. Même puis autre discours de mince veille quarante.
Fille retomber air accuser. Suivant dans bien sous oncle vent. Travail jour refuser nuit réel."""
msg = f"""\
From: {fromaddr}\r\n\
To: {", ".join(toaddrs)}\r\n\
Subject: {sujet}\r\n\
Date: {formatdate(localtime=True)}\r\n\
\r\n\
{message}"""
try:
    server.sendmail(fromaddr, toaddrs, msg)
except smtplib.SMTPException as e:
    print(e)
server.quit()

Python: Exporter la liste des packages

Pour sauvegarder dans un fichier requirements.txt tous les packages installés dans l'environnement Python:

$ python -m pip list --format freeze > requirements.txt

Et pour restaurer la liste complète, pour une nouvelle installation, par exemple:

$ python -m pip install -U -r .\requirements.txt

 

Python: Générer un fichier gpx

Un fichier GPX est un ensemble de coordonnées géographiques qui permet de créer des traces ou des itinéraires.

Pour utiliser ce script il est nécessaire d'installer les modules python suivants:

# python3 -m pip install --upgrade geopy requests dateutil bs4 numpy
#!/usr/bin/python3
# -*- coding: UTF-8 -*-

from geopy.geocoders import Nominatim
import requests
from dateutil.relativedelta import relativedelta
from bs4 import BeautifulSoup as bs
import numpy as np

cities = ['nantes, france', 'ancenis, france', 'angers, france']
geolocator = Nominatim(user_agent="Python3")
LAT = []
LNG = []
url = "https://routing.openstreetmap.de/routed-car/route/v1/driving/{frlng},{frlat};{tolng},{tolat}"
params = dict(overview='false', geometries='polyline', steps='true')
xml = bs(features='xml')
gpx = xml.new_tag('gpx')
gpx.attrs = {'creator':"Script Python", 'version':"1.1",\
             'xmlns':"http://www.topografix.com/GPX/1/1", 'xmlns:xsi':"http://www.w3.org/2001/XMLSchema-instance",\
             'xsi:schemaLocation':"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"}
trkl = []

for idxcity in enumerate(cities[:-1]):
    idx, city = idxcity
    FROM = geolocator.geocode(city)
    TO = geolocator.geocode(cities[idx+1])
    datas = dict(frlat=FROM.latitude, frlng=FROM.longitude, tolat=TO.latitude, tolng=TO.longitude)
    req1 = requests.get(url.format(**datas), params)
    js = req1.json()
    steps = js.get('routes')[0].get('legs')[0].get('steps')
    distance = round(js.get('routes')[0].get('distance') / 1000, 0)
    duration = relativedelta(seconds=js.get('routes')[0].get('duration'))
    frname = FROM.raw.get('display_name')
    toname = TO.raw.get('display_name')
    print(f'Itinéraire entre {frname} et {toname}.')
    wpt = xml.new_tag('wpt')
    wpt.attrs = {'lat':FROM.latitude , 'lon':FROM.longitude}
    wptname = xml.new_tag('name')
    wptname.string = frname
    wpt.append(wptname)
    gpx.append(wpt)
    trk = xml.new_tag('trk')
    name = xml.new_tag('name')
    name.string = f'{frname} - {toname}'
    desc = xml.new_tag('desc')
    desc.string = f'{distance} km, {int(duration.hours)}:{int(duration.minutes)}'
    trkseg = xml.new_tag('trkseg')
    for x in steps:
        for y in x['intersections']:
            lon, lat = y['location']
            LAT.append(lat)
            LNG.append(lon)
            trkpt = xml.new_tag('trkpt')
            trkpt.attrs = dict(lat=lat, lon=lon)
            trkseg.append(trkpt)
    trk.append(name)
    trk.append(desc)
    trk.append(trkseg)
    trkl.append(trk)

wpt = xml.new_tag('wpt')
wpt.attrs = {'lat':TO.latitude , 'lon':TO.longitude}
wptname = xml.new_tag('name')
wptname.string = toname
wpt.append(wptname)
gpx.append(wpt)
for trk in trkl:
    gpx.append(trk)
xml.append(gpx)

print(xml.prettify())
print(f'Position centrale: {np.mean(LAT)},{np.mean(LNG)}')

Dans mon exemple, je souhaite créer un itinéraire Nantes - Ancenis - Angers

Ce script va générer un contenu xml comme ceci:

<?xml version="1.0" encoding="utf-8"?>
<gpx creator="Script Python" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
 <wpt lat="47.2186371" lon="-1.5541362">
  <name>
   Nantes, Loire-Atlantique, Pays de la Loire, France métropolitaine, France
  </name>
 </wpt>
 <wpt lat="47.3648141" lon="-1.1816088">
  <name>
   Ancenis, Châteaubriant-Ancenis, Loire-Atlantique, Pays de la Loire, France métropolitaine, 44150, France
  </name>
 </wpt>
 <wpt lat="47.4739884" lon="-0.5515588">
  <name>
   Angers, Maine-et-Loire, Pays de la Loire, France métropolitaine, France
  </name>
 </wpt>
 <trk>
  <name>
   Nantes, Loire-Atlantique, Pays de la Loire, France métropolitaine, France - Ancenis, Châteaubriant-Ancenis, Loire-Atlantique, Pays de la Loire, France métropolitaine, 44150, France
  </name>
  <desc>
   44.0 km, 0:37
  </desc>
  <trkseg>
   <trkpt lat="47.218536" lon="-1.554075"/>
   <trkpt lat="47.218427" lon="-1.55398"/>
   <trkpt lat="47.218355" lon="-1.553938"/>
   <trkpt lat="47.218337" lon="-1.553975"/>
   <trkpt lat="47.218143" lon="-1.553968"/>
   <trkpt lat="47.218345" lon="-1.553476"/>
   <trkpt lat="47.217903" lon="-1.553421"/>
   <trkpt lat="47.217333" lon="-1.553389"/>
   <trkpt lat="47.21738" lon="-1.552416"/>
   <trkpt lat="47.217183" lon="-1.55225"/>
   <trkpt lat="47.216706" lon="-1.551836"/>
   <trkpt lat="47.214756" lon="-1.550238"/>
   <trkpt lat="47.214525" lon="-1.55024"/>
   <trkpt lat="47.214364" lon="-1.55024"/>
   <trkpt lat="47.214335" lon="-1.550143"/>
   <trkpt lat="47.214401" lon="-1.549983"/>
   <trkpt lat="47.215347" lon="-1.547803"/>
   <trkpt lat="47.215441" lon="-1.547616"/>
   <trkpt lat="47.215485" lon="-1.547787"/>
   <trkpt lat="47.215777" lon="-1.547168"/>
   <trkpt lat="47.214924" lon="-1.546208"/>
   <trkpt lat="47.214733" lon="-1.546091"/>
   <trkpt lat="47.214651" lon="-1.546014"/>
   <trkpt lat="47.213826" lon="-1.545304"/>
   <trkpt lat="47.21329" lon="-1.54487"/>
   <trkpt lat="47.21323" lon="-1.544817"/>
   <trkpt lat="47.213195" lon="-1.544786"/>
   <trkpt lat="47.211943" lon="-1.543759"/>
   <trkpt lat="47.211785" lon="-1.543751"/>
   <trkpt lat="47.21172" lon="-1.543605"/>
   <trkpt lat="47.211718" lon="-1.543567"/>
   <trkpt lat="47.211794" lon="-1.543374"/>
   <trkpt lat="47.211883" lon="-1.543345"/>
   <trkpt lat="47.212079" lon="-1.542566"/>
   <trkpt lat="47.212058" lon="-1.542522"/>
   <trkpt lat="47.212074" lon="-1.542362"/>
   <trkpt lat="47.212166" lon="-1.542314"/>
   <trkpt lat="47.212658" lon="-1.541764"/>
   <trkpt lat="47.213442" lon="-1.540108"/>
   <trkpt lat="47.213524" lon="-1.539675"/>
   <trkpt lat="47.213516" lon="-1.539587"/>
   <trkpt lat="47.213565" lon="-1.539412"/>
   <trkpt lat="47.213187" lon="-1.537239"/>
   <trkpt lat="47.21313" lon="-1.53719"/>
   <trkpt lat="47.213121" lon="-1.537007"/>
   <trkpt lat="47.213037" lon="-1.536746"/>
   <trkpt lat="47.212985" lon="-1.536673"/>
   <trkpt lat="47.213028" lon="-1.536488"/>
   <trkpt lat="47.213094" lon="-1.536477"/>
   <trkpt lat="47.213187" lon="-1.534501"/>
   <trkpt lat="47.21322" lon="-1.533176"/>
   <trkpt lat="47.213208" lon="-1.532928"/>
   <trkpt lat="47.213599" lon="-1.529777"/>
   <trkpt lat="47.21352" lon="-1.529614"/>
   <trkpt lat="47.213549" lon="-1.529374"/>
   <trkpt lat="47.213601" lon="-1.52929"/>
   <trkpt lat="47.213848" lon="-1.527269"/>
   <trkpt lat="47.213891" lon="-1.527133"/>
   <trkpt lat="47.214444" lon="-1.525283"/>
   <trkpt lat="47.214487" lon="-1.525111"/>
   <trkpt lat="47.215749" lon="-1.521155"/>
   <trkpt lat="47.215802" lon="-1.521012"/>
   <trkpt lat="47.216484" lon="-1.519088"/>
   <trkpt lat="47.21657" lon="-1.518772"/>
   <trkpt lat="47.216602" lon="-1.518664"/>
   <trkpt lat="47.218048" lon="-1.515754"/>
   <trkpt lat="47.218123" lon="-1.515623"/>
   <trkpt lat="47.218259" lon="-1.515588"/>
   <trkpt lat="47.218354" lon="-1.515663"/>
   <trkpt lat="47.219157" lon="-1.516011"/>
   <trkpt lat="47.219943" lon="-1.515468"/>
   <trkpt lat="47.232161" lon="-1.485789"/>
   <trkpt lat="47.232229" lon="-1.485681"/>
   <trkpt lat="47.235333" lon="-1.482751"/>
   <trkpt lat="47.2354" lon="-1.482516"/>
   <trkpt lat="47.235546" lon="-1.482416"/>
   <trkpt lat="47.237493" lon="-1.479355"/>
   <trkpt lat="47.237476" lon="-1.479187"/>
   <trkpt lat="47.237571" lon="-1.47894"/>
   <trkpt lat="47.237679" lon="-1.478875"/>
   <trkpt lat="47.237841" lon="-1.478929"/>
   <trkpt lat="47.237865" lon="-1.478955"/>
   <trkpt lat="47.23968" lon="-1.47769"/>
   <trkpt lat="47.252684" lon="-1.477342"/>
   <trkpt lat="47.255057" lon="-1.47844"/>
   <trkpt lat="47.26823" lon="-1.479836"/>
   <trkpt lat="47.270611" lon="-1.479649"/>
   <trkpt lat="47.273086" lon="-1.479482"/>
   <trkpt lat="47.287258" lon="-1.479302"/>
   <trkpt lat="47.290543" lon="-1.476443"/>
   <trkpt lat="47.291107" lon="-1.471735"/>
   <trkpt lat="47.399341" lon="-1.200555"/>
   <trkpt lat="47.39931" lon="-1.195302"/>
   <trkpt lat="47.399266" lon="-1.192826"/>
   <trkpt lat="47.399277" lon="-1.192706"/>
   <trkpt lat="47.399304" lon="-1.192254"/>
   <trkpt lat="47.399387" lon="-1.191168"/>
   <trkpt lat="47.399341" lon="-1.190568"/>
   <trkpt lat="47.399294" lon="-1.190479"/>
   <trkpt lat="47.396962" lon="-1.190001"/>
   <trkpt lat="47.395567" lon="-1.189791"/>
   <trkpt lat="47.395442" lon="-1.189771"/>
   <trkpt lat="47.394798" lon="-1.189592"/>
   <trkpt lat="47.394688" lon="-1.189555"/>
   <trkpt lat="47.391687" lon="-1.188792"/>
   <trkpt lat="47.391615" lon="-1.18885"/>
   <trkpt lat="47.391446" lon="-1.188684"/>
   <trkpt lat="47.391328" lon="-1.188495"/>
   <trkpt lat="47.390855" lon="-1.18821"/>
   <trkpt lat="47.390459" lon="-1.188124"/>
   <trkpt lat="47.390307" lon="-1.188215"/>
   <trkpt lat="47.39019" lon="-1.18816"/>
   <trkpt lat="47.390099" lon="-1.187937"/>
   <trkpt lat="47.390099" lon="-1.18787"/>
   <trkpt lat="47.386784" lon="-1.185549"/>
   <trkpt lat="47.386387" lon="-1.185516"/>
   <trkpt lat="47.386278" lon="-1.185596"/>
   <trkpt lat="47.386194" lon="-1.185538"/>
   <trkpt lat="47.386165" lon="-1.185462"/>
   <trkpt lat="47.385743" lon="-1.185042"/>
   <trkpt lat="47.383247" lon="-1.183994"/>
   <trkpt lat="47.382652" lon="-1.183919"/>
   <trkpt lat="47.382623" lon="-1.183975"/>
   <trkpt lat="47.382522" lon="-1.184013"/>
   <trkpt lat="47.382465" lon="-1.18397"/>
   <trkpt lat="47.382435" lon="-1.18385"/>
   <trkpt lat="47.37964" lon="-1.182634"/>
   <trkpt lat="47.378879" lon="-1.182151"/>
   <trkpt lat="47.377554" lon="-1.181295"/>
   <trkpt lat="47.377402" lon="-1.181282"/>
   <trkpt lat="47.377354" lon="-1.181357"/>
   <trkpt lat="47.377255" lon="-1.181394"/>
   <trkpt lat="47.377135" lon="-1.181101"/>
   <trkpt lat="47.376946" lon="-1.180899"/>
   <trkpt lat="47.375527" lon="-1.179969"/>
   <trkpt lat="47.373767" lon="-1.178955"/>
   <trkpt lat="47.373402" lon="-1.178983"/>
   <trkpt lat="47.373302" lon="-1.179059"/>
   <trkpt lat="47.373222" lon="-1.178978"/>
   <trkpt lat="47.372174" lon="-1.178905"/>
   <trkpt lat="47.372112" lon="-1.178948"/>
   <trkpt lat="47.372092" lon="-1.178902"/>
   <trkpt lat="47.371988" lon="-1.178904"/>
   <trkpt lat="47.3704" lon="-1.178883"/>
   <trkpt lat="47.370202" lon="-1.178866"/>
   <trkpt lat="47.369961" lon="-1.178839"/>
   <trkpt lat="47.369467" lon="-1.178797"/>
   <trkpt lat="47.369026" lon="-1.178756"/>
   <trkpt lat="47.368278" lon="-1.178584"/>
   <trkpt lat="47.366732" lon="-1.178582"/>
   <trkpt lat="47.3667" lon="-1.178583"/>
   <trkpt lat="47.366514" lon="-1.178598"/>
   <trkpt lat="47.365991" lon="-1.1786"/>
   <trkpt lat="47.365403" lon="-1.178805"/>
   <trkpt lat="47.364949" lon="-1.178912"/>
   <trkpt lat="47.364547" lon="-1.178981"/>
   <trkpt lat="47.364474" lon="-1.17934"/>
   <trkpt lat="47.364424" lon="-1.17992"/>
   <trkpt lat="47.364136" lon="-1.180728"/>
   <trkpt lat="47.36416" lon="-1.180945"/>
   <trkpt lat="47.364268" lon="-1.181819"/>
   <trkpt lat="47.364763" lon="-1.18192"/>
  </trkseg>
 </trk>
 <trk>
  <name>
   Ancenis, Châteaubriant-Ancenis, Loire-Atlantique, Pays de la Loire, France métropolitaine, 44150, France - Angers, Maine-et-Loire, Pays de la Loire, France métropolitaine, France
  </name>
  <desc>
   57.0 km, 0:41
  </desc>
  <trkseg>
   <trkpt lat="47.364763" lon="-1.18192"/>
   <trkpt lat="47.364848" lon="-1.18196"/>
   <trkpt lat="47.365275" lon="-1.182104"/>
   <trkpt lat="47.365965" lon="-1.182286"/>
   <trkpt lat="47.36612" lon="-1.18113"/>
   <trkpt lat="47.366403" lon="-1.179826"/>
   <trkpt lat="47.366732" lon="-1.178582"/>
   <trkpt lat="47.368278" lon="-1.178584"/>
   <trkpt lat="47.369026" lon="-1.178756"/>
   <trkpt lat="47.369467" lon="-1.178797"/>
   <trkpt lat="47.369961" lon="-1.178839"/>
   <trkpt lat="47.370202" lon="-1.178866"/>
   <trkpt lat="47.3704" lon="-1.178883"/>
   <trkpt lat="47.371988" lon="-1.178904"/>
   <trkpt lat="47.372092" lon="-1.178902"/>
   <trkpt lat="47.37213" lon="-1.178835"/>
   <trkpt lat="47.372174" lon="-1.178905"/>
   <trkpt lat="47.373149" lon="-1.178876"/>
   <trkpt lat="47.373211" lon="-1.178881"/>
   <trkpt lat="47.373272" lon="-1.178766"/>
   <trkpt lat="47.373372" lon="-1.17878"/>
   <trkpt lat="47.373415" lon="-1.178882"/>
   <trkpt lat="47.373767" lon="-1.178955"/>
   <trkpt lat="47.375527" lon="-1.179969"/>
   <trkpt lat="47.376946" lon="-1.180899"/>
   <trkpt lat="47.377202" lon="-1.180991"/>
   <trkpt lat="47.377317" lon="-1.180973"/>
   <trkpt lat="47.377418" lon="-1.18115"/>
   <trkpt lat="47.378879" lon="-1.182151"/>
   <trkpt lat="47.37964" lon="-1.182634"/>
   <trkpt lat="47.382469" lon="-1.183726"/>
   <trkpt lat="47.38252" lon="-1.183687"/>
   <trkpt lat="47.382627" lon="-1.183736"/>
   <trkpt lat="47.382649" lon="-1.183773"/>
   <trkpt lat="47.383247" lon="-1.183994"/>
   <trkpt lat="47.385743" lon="-1.185042"/>
   <trkpt lat="47.386187" lon="-1.185301"/>
   <trkpt lat="47.386324" lon="-1.18524"/>
   <trkpt lat="47.386395" lon="-1.185331"/>
   <trkpt lat="47.386784" lon="-1.185549"/>
   <trkpt lat="47.390184" lon="-1.18765"/>
   <trkpt lat="47.390236" lon="-1.187609"/>
   <trkpt lat="47.390418" lon="-1.187636"/>
   <trkpt lat="47.390457" lon="-1.187677"/>
   <trkpt lat="47.390521" lon="-1.187884"/>
   <trkpt lat="47.390855" lon="-1.18821"/>
   <trkpt lat="47.391328" lon="-1.188495"/>
   <trkpt lat="47.391505" lon="-1.18847"/>
   <trkpt lat="47.391608" lon="-1.188436"/>
   <trkpt lat="47.391718" lon="-1.188566"/>
   <trkpt lat="47.394176" lon="-1.18923"/>
   <trkpt lat="47.394707" lon="-1.189419"/>
   <trkpt lat="47.394816" lon="-1.189455"/>
   <trkpt lat="47.396101" lon="-1.189755"/>
   <trkpt lat="47.396578" lon="-1.189826"/>
   <trkpt lat="47.398083" lon="-1.190038"/>
   <trkpt lat="47.39932" lon="-1.190194"/>
   <trkpt lat="47.399436" lon="-1.190105"/>
   <trkpt lat="47.399616" lon="-1.190257"/>
   <trkpt lat="47.399625" lon="-1.190297"/>
   <trkpt lat="47.399598" lon="-1.190518"/>
   <trkpt lat="47.399572" lon="-1.190561"/>
   <trkpt lat="47.399396" lon="-1.192017"/>
   <trkpt lat="47.399366" lon="-1.192722"/>
   <trkpt lat="47.399355" lon="-1.192841"/>
   <trkpt lat="47.39927" lon="-1.194329"/>
   <trkpt lat="47.399938" lon="-1.194827"/>
   <trkpt lat="47.401803" lon="-1.191068"/>
   <trkpt lat="47.434482" lon="-0.816724"/>
   <trkpt lat="47.433855" lon="-0.809441"/>
   <trkpt lat="47.46242" lon="-0.695047"/>
   <trkpt lat="47.467222" lon="-0.677305"/>
   <trkpt lat="47.466628" lon="-0.674247"/>
   <trkpt lat="47.462466" lon="-0.64317"/>
   <trkpt lat="47.4658" lon="-0.631566"/>
   <trkpt lat="47.466724" lon="-0.628554"/>
   <trkpt lat="47.46786" lon="-0.624756"/>
   <trkpt lat="47.469056" lon="-0.620722"/>
   <trkpt lat="47.470005" lon="-0.617478"/>
   <trkpt lat="47.469578" lon="-0.612166"/>
   <trkpt lat="47.468996" lon="-0.605593"/>
   <trkpt lat="47.469353" lon="-0.597749"/>
   <trkpt lat="47.469475" lon="-0.594958"/>
   <trkpt lat="47.468084" lon="-0.585639"/>
   <trkpt lat="47.464676" lon="-0.574428"/>
   <trkpt lat="47.465802" lon="-0.568425"/>
   <trkpt lat="47.46848" lon="-0.564903"/>
   <trkpt lat="47.469906" lon="-0.562375"/>
   <trkpt lat="47.470104" lon="-0.561996"/>
   <trkpt lat="47.471074" lon="-0.560347"/>
   <trkpt lat="47.471585" lon="-0.559436"/>
   <trkpt lat="47.472442" lon="-0.558075"/>
   <trkpt lat="47.474047" lon="-0.556018"/>
   <trkpt lat="47.475157" lon="-0.555216"/>
   <trkpt lat="47.476197" lon="-0.554405"/>
   <trkpt lat="47.476813" lon="-0.553653"/>
   <trkpt lat="47.476842" lon="-0.553634"/>
   <trkpt lat="47.477151" lon="-0.553118"/>
   <trkpt lat="47.476517" lon="-0.551532"/>
   <trkpt lat="47.476224" lon="-0.550865"/>
   <trkpt lat="47.47594" lon="-0.550269"/>
   <trkpt lat="47.475462" lon="-0.549161"/>
   <trkpt lat="47.475096" lon="-0.549804"/>
   <trkpt lat="47.475076" lon="-0.549856"/>
   <trkpt lat="47.474792" lon="-0.550535"/>
   <trkpt lat="47.474586" lon="-0.551368"/>
   <trkpt lat="47.474561" lon="-0.55151"/>
   <trkpt lat="47.474384" lon="-0.552383"/>
   <trkpt lat="47.473949" lon="-0.551608"/>
  </trkseg>
 </trk>
</gpx>

Ce contenu xml peut ensuite être chargé dans n'importe quelle application ou site internet utilisant ce genre de fichier.

Etiquettes: 

Python: Générer une liste des jours ouvrés sans les jours fériés

Pouvoir générer la liste de tous les jours ouvrés d'un mois, d'un trimestre, d'un semestre ou d'une année et en excluant les jours fériés existants.

Pour cela je vais également utiliser la classe JoursFeries créée dans un précédent article.

from dateutil.parser import parse
from dateutil.rrule import rrule, DAILY
from dateutil.relativedelta import relativedelta
from dateutil.relativedelta import MO, TU, WE, TH, FR

dtstart = parse('2022-01-01')
list_jours_ouvres = list(
    map(
        lambda x: x.date(),
        rrule(DAILY, dtstart=dtstart, until=dtstart + relativedelta(months=4, day=1, days=-1),
              byweekday=[MO, TU, WE, TH, FR])
    )
)

Je vais donc générer une liste de tous les jours ouvrés, du lundi au vendredi, du 1er janvier au 30 avril (4 mois).

>>> list_jours_ouvres
[datetime.date(2022, 1, 3),
 datetime.date(2022, 1, 4),
 datetime.date(2022, 1, 5),
 datetime.date(2022, 1, 6),
 datetime.date(2022, 1, 7),
 datetime.date(2022, 1, 10),
 datetime.date(2022, 1, 11),
 datetime.date(2022, 1, 12),
 datetime.date(2022, 1, 13),
 ...
 datetime.date(2022, 4, 18),
 datetime.date(2022, 4, 19),
 datetime.date(2022, 4, 20),
 datetime.date(2022, 4, 21),
 datetime.date(2022, 4, 22),
 datetime.date(2022, 4, 25),
 datetime.date(2022, 4, 26),
 datetime.date(2022, 4, 27),
 datetime.date(2022, 4, 28),
 datetime.date(2022, 4, 29)]
>>> len(list_jours_ouvres)
85

J'ai donc une liste comprenant 85 jours ouvrés du lundi au vendredi du 3 janvier au 29 avril.

Pour supprimer les jours fériés de la liste

>>> set(list_jours_ouvres) - set(JoursFeries(dtstart.year).to_list())
{datetime.date(2022, 1, 3),
 datetime.date(2022, 1, 4),
 datetime.date(2022, 1, 5),
 datetime.date(2022, 1, 6),
 datetime.date(2022, 1, 7),
 datetime.date(2022, 1, 10),
 datetime.date(2022, 1, 11),
 datetime.date(2022, 1, 12),
 datetime.date(2022, 1, 13),
 datetime.date(2022, 1, 14),
 datetime.date(2022, 1, 17),
 datetime.date(2022, 1, 18),
 datetime.date(2022, 1, 19),
 datetime.date(2022, 1, 20),
 datetime.date(2022, 1, 21),
 datetime.date(2022, 1, 24),
 ...
 datetime.date(2022, 4, 13),
 datetime.date(2022, 4, 14),
 datetime.date(2022, 4, 15),
 datetime.date(2022, 4, 19),
 datetime.date(2022, 4, 20),
 datetime.date(2022, 4, 21),
 datetime.date(2022, 4, 22),
 datetime.date(2022, 4, 25),
 datetime.date(2022, 4, 26),
 datetime.date(2022, 4, 27),
 datetime.date(2022, 4, 28),
 datetime.date(2022, 4, 29)}
>>> len(set(list_jours_ouvres) - set(JoursFeries(dtstart.year).to_list()))
84

Résultat, 1 jour a été supprimé de la liste.
Il s'agit du lundi de Pâques le 18 avril 2022.

>>> for x in JoursFeries(dtstart.year).proprietes:
    print(f"{x:<20} {getattr(JoursFeries(dtstart.year), x).strftime('%a %d %b %Y')}")

JOUR_DE_L_AN         Sat 01 Jan 2022
PAQUES               Sun 17 Apr 2022
LUNDI_DE_PAQUES      Mon 18 Apr 2022
FETE_DU_TRAVAIL      Sun 01 May 2022
VICTOIRE_1945        Sun 08 May 2022
ASCENSION            Thu 26 May 2022
PENTECOTE            Sun 05 Jun 2022
LUNDI_DE_PENTECOTE   Mon 06 Jun 2022
FETE_NATIONALE       Thu 14 Jul 2022
ASSOMPTION           Mon 15 Aug 2022
TOUSSAINT            Tue 01 Nov 2022
ARMISTICE_1918       Fri 11 Nov 2022
NOEL                 Sun 25 Dec 2022

 

Python: Indenter un contenu JSON

Voici un petit script Python qui me permet d'afficher à l'écran n'importe quel contenu JSON correctement indenté (afin de faciliter la lisibilité).

linkprettyJson.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import json, sys
from pprint import pprint

if not sys.stdin.isatty():
    JSON = json.loads(sys.stdin.read())
    pprint(JSON)
else:
    print("Use this script with a pipe command.")
    print("")
    print("echo '...' | python3 prettyJson.py")
    print("or")
    print("cat ... | python3 prettyJson.py")

Par exemple, avec un flux JSON non formaté:

# cat flux.json
{"time": "2016-06-29 15:54","countryName": "Austria","sunset": "2016-06-29 21:18","rawOffset": 1,"dstOffset": 2,"countryCode": "AT","gmtOffset": 1,"lng": 10.2,"sunrise": "2016-06-29 05:27","timezoneId": "Europe/Vienna","lat": 47.01}

Une fois traité par le script:

# cat flux.json | python3 prettyJson.py
{'countryCode': 'AT',
 'countryName': 'Austria',
 'dstOffset': 2,
 'gmtOffset': 1,
 'lat': 47.01,
 'lng': 10.2,
 'rawOffset': 1,
 'sunrise': '2016-06-29 05:27',
 'sunset': '2016-06-29 21:18',
 'time': '2016-06-29 15:54',
 'timezoneId': 'Europe/Vienna'}

Fonctionne également avec la commande CURL:

# curl "http://api.geonames.org/citiesJSON?formatted=true&north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo&style=full" -o - -s | python3 prettyJson.py
{'geonames': [{'countrycode': 'MX',
               'fcl': 'P',
               'fclName': 'city, village,...',
               'fcode': 'PPLC',
               'fcodeName': 'capital of a political entity',
               'geonameId': 3530597,
               'lat': 19.428472427036,
               'lng': -99.12766456604,
               'name': 'Mexiko-Stadt',
               'population': 12294193,
               'toponymName': 'Mexico City',
               'wikipedia': 'en.wikipedia.org/wiki/Mexico_City'},
              {'countrycode': 'CN',
               'fcl': 'P',
               'fclName': 'city, village,...',
               'fcode': 'PPLC',
               'fcodeName': 'capital of a political entity',
               'geonameId': 1816670,
               'lat': 39.9074977414405,
               'lng': 116.397228240967,
               'name': 'Peking',
               'population': 11716620,
               'toponymName': 'Beijing',
               'wikipedia': 'en.wikipedia.org/wiki/Beijing'}]}
Etiquettes: 

Python: Initialiser une liste à l'aide de la fonction INPUT

Voici une petite fonction permettant d'initialiser une liste en saisissant les valeurs à la volée à l'aide de la fonction input.

Certe inutile pour une courte liste [1,2,3,4], mais vraiment très pratique pour une longue liste et contenant surtout des données alpha-numériques (évite de saisir les quotes entourant la chaine de texte)

Compatible Python2 et Python3

>>> def initializeList():
    l = []
    # Pour la compatibilité Python2 et Python3
    _input = input
    if hasattr(__builtins__, 'raw_input'):
        _input = raw_input
    # Boucle infinie pour remplir la liste
    while True:
        d = _input("--> ").strip()
        if d == '':
            # On quitte si la valeur est vide
            break
        else:
            # On tente une conversion en int
            try:
                l.append(int(d))
            except ValueError:
                # Si erreur, on tente une conversion en float
                try:
                    l.append(float(d))
                except ValueError:
                    # Sinon, on laisse la valeur au format chaine de texte
                    l.append(d)
    # On retourne la liste
    return l

>>> myList = initializeList()
--> 1
--> 2
--> 3
--> 4
--> 5
--> 6
--> a
--> z
--> er
--> r
--> t
--> y
--> fg
--> d
-->
>>> print(myList)
[1, 2, 3, 4, 5, 6, 'a', 'z', 'er', 'r', 't', 'y', 'fg', 'd']
>>>

Ca peut toujours servir

Python: Insérer du contenu binaire dans un champ d'une table Sqlite3

Pour l'exemple, je vais utiliser un base de donnée sqlite3 en mémoire.

>>> import sqlite3
>>> from pathlib import Path
>>> con = sqlite3.connect(':memory:')
>>> cur = con.cursor()
>>> cur.execute('create table datas (ID INTEGER primary key, IMAGE BLOB)')
>>> p = Path(r'C:\Users\toto\Pictures')
>>> f = p / 'toto.png'
>>> cur.execute("insert into datas (IMAGE) values (?)", (sqlite3.Binary(f.read_bytes()), ))
>>> con.commit()

 

Etiquettes: 

Python: Jours fériés français

Voici une classe que j'ai développé afin de gérer efficacement les jours fériés français.

Elle pourra être utilisée ultérieurement dans différentes applications.

Voici le code:

from enum import IntEnum
from datetime import datetime, date
from dateutil.easter import easter, EASTER_WESTERN
from dateutil.relativedelta import relativedelta
import json
from collections import namedtuple
import serpy

class Mois(IntEnum):
    
    JANVIER   = 1
    FEVRIER   = 2
    MARS      = 3
    AVRIL     = 4
    MAI       = 5
    JUIN      = 6
    JUILLET   = 7
    AOUT      = 8
    SEPTEMBRE = 9
    OCTOBRE   = 10
    NOVEMBRE  = 11
    DECEMBRE  = 12
            
class JoursFeries(object):
    
    def __init__(self, annee: int = datetime.now().year):
        self.CURRENT_YEAR = int(annee)
            
    @property
    def CURRENT_YEAR(self) -> int:
        return self._annee

    @CURRENT_YEAR.setter
    def CURRENT_YEAR(self, value: int) -> None:
        self._annee = value
    
    @property
    def JOUR_DE_L_AN(self) -> date:
        return date(self.CURRENT_YEAR, Mois.JANVIER, 1)
    
    @property
    def PAQUES(self) -> date:
        return easter(self.CURRENT_YEAR, method=EASTER_WESTERN)
    
    @property
    def LUNDI_DE_PAQUES(self) -> date:
        return self.PAQUES + relativedelta(days=1)
    
    @property
    def FETE_DU_TRAVAIL(self) -> date:
        return date(self.CURRENT_YEAR, Mois.MAI, 1)
    
    @property
    def VICTOIRE_1945(self) -> date:
        return date(self.CURRENT_YEAR, Mois.MAI, 8)
    
    @property
    def ASCENSION(self) -> date:
        return self.PAQUES + relativedelta(days=39)
    
    @property
    def PENTECOTE(self) -> date:
        return self.PAQUES + relativedelta(days=49)
    
    @property
    def LUNDI_DE_PENTECOTE(self) -> date:
        return self.PENTECOTE + relativedelta(days=1)
    
    @property
    def FETE_NATIONALE(self) -> date:
        return date(self.CURRENT_YEAR, Mois.JUILLET, 14)
    
    @property
    def ASSOMPTION(self) -> date:
        return date(self.CURRENT_YEAR, Mois.AOUT, 15)
    
    @property
    def TOUSSAINT(self) -> date:
        return date(self.CURRENT_YEAR, Mois.NOVEMBRE, 1)
    
    @property
    def ARMISTICE_1918(self) -> date:
        return date(self.CURRENT_YEAR, Mois.NOVEMBRE, 11)
    
    @property
    def NOEL(self) -> date:
        return date(self.CURRENT_YEAR, Mois.DECEMBRE, 25)
    
    @property
    def proprietes(self) -> list:
        return list(self.dumps().keys())
    
    def to_list(self) -> list:
        return [getattr(self, x) for x in self.dumps().keys()]
    
    def __str__(self) -> str:
        return '\n'.join([f'{x:<20s}: {getattr(self, x)}' for x in self.dumps().keys()])
    
    def __repr__(self) ->str:
        return json.dumps(self.dumps(), indent=4, ensure_ascii=False)
        
    def dumps(self) -> dict:
        return JoursFeriesSerialize(self).data
    
    def to_namedtuple(self) -> namedtuple:
        return namedtuple('JourFeries', self.dumps().keys())(**self.dumps())
    
class JoursFeriesSerialize(serpy.Serializer):
    
    JOUR_DE_L_AN       = serpy.StrField()
    PAQUES             = serpy.StrField()
    LUNDI_DE_PAQUES    = serpy.StrField()
    FETE_DU_TRAVAIL    = serpy.StrField()
    VICTOIRE_1945      = serpy.StrField()
    ASCENSION          = serpy.StrField()
    PENTECOTE          = serpy.StrField()
    LUNDI_DE_PENTECOTE = serpy.StrField()
    FETE_NATIONALE     = serpy.StrField()
    ASSOMPTION         = serpy.StrField()
    TOUSSAINT          = serpy.StrField()
    ARMISTICE_1918     = serpy.StrField()
    NOEL               = serpy.StrField()

La classe Mois énumère tous les mois de l'année grâce au package enum.IntEnum.

La classe JoursFeriesSerialize permet de sérialiser les données de la classe JoursFeries grâce au package serpy.

Pour finir, la classe JoursFeries qui permet de générer tous les jours fériés de l'année en cours ou de celle passée en paramètre au constructeur de la classe.

Le jour férié correspondant à Pâques est récupéré grâce au package dateutil.easter.easter et tous les jours fériés ayant un rapport avec Pâques sont générés grâce au package dateutil.relativedelta.relativedelta.

Pour l'utiliser, rien de plus simple:

>>> jf = JoursFeries()
>>> jf
{
    "JOUR_DE_L_AN": "2022-01-01",
    "PAQUES": "2022-04-17",
    "LUNDI_DE_PAQUES": "2022-04-18",
    "FETE_DU_TRAVAIL": "2022-05-01",
    "VICTOIRE_1945": "2022-05-08",
    "ASCENSION": "2022-05-26",
    "PENTECOTE": "2022-06-05",
    "LUNDI_DE_PENTECOTE": "2022-06-06",
    "FETE_NATIONALE": "2022-07-14",
    "ASSOMPTION": "2022-08-15",
    "TOUSSAINT": "2022-11-01",
    "ARMISTICE_1918": "2022-11-11",
    "NOEL": "2022-12-25"
}

Sans paramètre, la classe retourne tous les jours fériés de l'année en cours.

La méthode __repr__ permet d'afficher les jours fériés au format json (str).

Avec l'année passée en paramètre:

>>> jf = JoursFeries(2024)
>>> jf
{
    "JOUR_DE_L_AN": "2024-01-01",
    "PAQUES": "2024-03-31",
    "LUNDI_DE_PAQUES": "2024-04-01",
    "FETE_DU_TRAVAIL": "2024-05-01",
    "VICTOIRE_1945": "2024-05-08",
    "ASCENSION": "2024-05-09",
    "PENTECOTE": "2024-05-19",
    "LUNDI_DE_PENTECOTE": "2024-05-20",
    "FETE_NATIONALE": "2024-07-14",
    "ASSOMPTION": "2024-08-15",
    "TOUSSAINT": "2024-11-01",
    "ARMISTICE_1918": "2024-11-11",
    "NOEL": "2024-12-25"
}

La méthode to_list retourne une liste contenant tous les jours fériés au format datetime.date

>>> jf.to_list()
[datetime.date(2022, 1, 1),
 datetime.date(2022, 4, 17),
 datetime.date(2022, 4, 18),
 datetime.date(2022, 5, 1),
 datetime.date(2022, 5, 8),
 datetime.date(2022, 5, 26),
 datetime.date(2022, 6, 5),
 datetime.date(2022, 6, 6),
 datetime.date(2022, 7, 14),
 datetime.date(2022, 8, 15),
 datetime.date(2022, 11, 1),
 datetime.date(2022, 11, 11),
 datetime.date(2022, 12, 25)]

Un print de l'objet:

>>> print(jf)
JOUR_DE_L_AN        : 2022-01-01
PAQUES              : 2022-04-17
LUNDI_DE_PAQUES     : 2022-04-18
FETE_DU_TRAVAIL     : 2022-05-01
VICTOIRE_1945       : 2022-05-08
ASCENSION           : 2022-05-26
PENTECOTE           : 2022-06-05
LUNDI_DE_PENTECOTE  : 2022-06-06
FETE_NATIONALE      : 2022-07-14
ASSOMPTION          : 2022-08-15
TOUSSAINT           : 2022-11-01
ARMISTICE_1918      : 2022-11-11
NOEL                : 2022-12-25

La méthode dumps retourne une représentation json (dict)

>>> jf.dumps()
{'JOUR_DE_L_AN': '2022-01-01',
 'PAQUES': '2022-04-17',
 'LUNDI_DE_PAQUES': '2022-04-18',
 'FETE_DU_TRAVAIL': '2022-05-01',
 'VICTOIRE_1945': '2022-05-08',
 'ASCENSION': '2022-05-26',
 'PENTECOTE': '2022-06-05',
 'LUNDI_DE_PENTECOTE': '2022-06-06',
 'FETE_NATIONALE': '2022-07-14',
 'ASSOMPTION': '2022-08-15',
 'TOUSSAINT': '2022-11-01',
 'ARMISTICE_1918': '2022-11-11',
 'NOEL': '2022-12-25'}

La méthode to_namedtuple retourne un objet collections.namedtuple

>>> jf.to_namedtuple()
JourFeries(JOUR_DE_L_AN='2022-01-01', PAQUES='2022-04-17', LUNDI_DE_PAQUES='2022-04-18', FETE_DU_TRAVAIL='2022-05-01', VICTOIRE_1945='2022-05-08', ASCENSION='2022-05-26', PENTECOTE='2022-06-05', LUNDI_DE_PENTECOTE='2022-06-06', FETE_NATIONALE='2022-07-14', ASSOMPTION='2022-08-15', TOUSSAINT='2022-11-01', ARMISTICE_1918='2022-11-11', NOEL='2022-12-25')

Pour afficher le jour férié correspondant à Pâques

>>> jf.PAQUES
datetime.date(2022, 4, 17)
>>> jf.PAQUES.year
2022
>>> jf.PAQUES.day
17
>>> jf.PAQUES.month
4
>>> jf.PAQUES.isocalendar()
(2022, 15, 7) # le 7ème jour de la semaine 15 de l'année 2022
>>> jf.PAQUES.isoformat()
2022-04-17
>>> jf.PAQUES.timetuple()
time.struct_time(tm_year=2022, tm_mon=4, tm_mday=17, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=6, tm_yday=107, tm_isdst=-1)

Tous les jours fériés disposent des mêmes fonctions.

Comme les jours fériés sont au format datetime.date, toutes les méthodes de l'objet datetime.date sont également accessibles.

La méthode proprietes affiche la liste des noms de tous les jours fériés

>>> jf.proprietes
['JOUR_DE_L_AN',
 'PAQUES',
 'LUNDI_DE_PAQUES',
 'FETE_DU_TRAVAIL',
 'VICTOIRE_1945',
 'ASCENSION',
 'PENTECOTE',
 'LUNDI_DE_PENTECOTE',
 'FETE_NATIONALE',
 'ASSOMPTION',
 'TOUSSAINT',
 'ARMISTICE_1918',
 'NOEL']

Ce qui permet de faire ceci

>>> for nom in jf.proprietes:
...    print(f"{nom:<20} {getattr(JoursFeries(dtstart.year), nom).strftime('%a %d %B %Y')}")
JOUR_DE_L_AN         Sat 01 Jan 2022
PAQUES               Sun 17 Apr 2022
LUNDI_DE_PAQUES      Mon 18 Apr 2022
FETE_DU_TRAVAIL      Sun 01 May 2022
VICTOIRE_1945        Sun 08 May 2022
ASCENSION            Thu 26 May 2022
PENTECOTE            Sun 05 Jun 2022
LUNDI_DE_PENTECOTE   Mon 06 Jun 2022
FETE_NATIONALE       Thu 14 Jul 2022
ASSOMPTION           Mon 15 Aug 2022
TOUSSAINT            Tue 01 Nov 2022
ARMISTICE_1918       Fri 11 Nov 2022
NOEL                 Sun 25 Dec 2022

Vraiment super pratique.

Python: La class avec Serpy

Serpy est un package Python qui permet sérialiser en json n'importe quel objet.

Serpy permet d'implémenter facilement et rapidement les méthodes __str__ et __repr__ d'une classe.

Il permet également d'implémenter une méthode dumps.

Exemple avec la classe suivante:

import serpy
import json
from decimal import Decimal

class Personne(object):

    def __init__(self, firstname: str, lastname: str, sexe: str, age: int, salaire: float, position: dict):
        self.firstname = firstname
        self.lastname = lastname
        self.sexe = sexe
        self.age = age
        self.salaire = salaire
        self.position = position

Cette classe permet de créer un objet Personne.
L'argument position est un dictionnaire dans lequel on renseigne une latitude et une longitude. Il devra contenir les 2 clés 'latitude' et 'longitude'.

Je vais maintenant créer deux nouvelles classes, qui vont permettre de sérialiser mon objet Personne et mon sous-objet Position.

class PositionSerializer(serpy.DictSerializer):
    latitude = serpy.FloatField()
    longitude = serpy.FloatField()


class PersonneSerializer(serpy.Serializer):
    firstname = serpy.StrField()
    lastname = serpy.StrField()
    sexe = serpy.MethodField()
    age = serpy.IntField()
    salaire = serpy.FloatField()
    position = PositionSerializer()

    def get_sexe(self, obj):
        if obj.sexe == 'M':
            return 'Homme'
        elif obj.sexe == 'F':
            return 'Femme'
        else:
            return 'N/A'

La classe PersonneSerializer étend la classe serpy.Serializer.
Elle reprend les différents arguments de la classe Personne en indiquant pour chaque argument son type Serpy (StrField, IntField, FloatField).
Le type particulier MethodField permet d'associer une méthode à l'argument afin d'effectuer des calculs spécifiques. La méthode doit être nommée comme le nom de l'argument précédé du terme 'get_'
Il est également possible d'associer à l'argument une classe qui permet de sérialiser le contenu de l'objet. Dans mon exemple, l'argument position est une dictionnaire qui contient 2 données décimales. La classe PositionSerializer étend la classe serpy.DictSerializer et reprend les 2 clés de mon objet dict position.

Je vais maintenant modifier ma classe Personne afin d'y ajouter les 3 méthodes __str__, __repr__ et dumps.

class Personne(object):

    def __init__(self, firstname: str, lastname: str, sexe: str, age: int, salaire: float, position: dict):
        self.firstname = firstname
        self.lastname = lastname
        self.sexe = sexe
        self.age = age
        self.salaire = salaire
        self.position = position

    def __str__(self):
        return json.dumps(PersonneSerializer(self).data, indent=4)

    def __repr__(self):
        return json.dumps(PersonneSerializer(self).data)

    def dumps(self):
        return PersonneSerializer(self).data

La méthode __str__ retourne mon objet sérialisé au format json via la classe PersonneSerializer.
La méthode __repr__ retourne également mon objet sérialisé au format json via la classe PersonneSerializer mais sans mise en forme.
La méthode dumps retourne mon objet sérialisé via la classe PersonneSerializer.

Et voici le résultat:

    personne1 = Personne(firstname='Matthieu', lastname='Lopes', sexe='M', age=43, salaire=1509.56,
                         position={'latitude': Decimal('-55.5232795'), 'longitude': Decimal('157.460948')})
    print(f'{" Personne 1 ":-^30}')
    print(repr(personne1))
    print(personne1)
    dumps = personne1.dumps()
    print(type(dumps))
    print(dumps)
    personne2 = Personne(firstname='Constance', lastname='Lopez', sexe='F', age=87, salaire=2056.35,
                         position={'latitude': Decimal('-56.710608'), 'longitude': Decimal('-154.540259')})
    print('')
    print(f'{" Personne 2 ":-^30}')
    print(repr(personne2))
    print(personne2)
    dumps = personne2.dumps()
    print(type(dumps))
    print(dumps)

--------- Personne 1 ---------
{"firstname": "Matthieu", "lastname": "Lopes", "sexe": "Homme", "age": 43, "salaire": 1509.56, "position": {"latitude": -55.5232795, "longitude": 157.460948}}
{
    "firstname": "Matthieu",
    "lastname": "Lopes",
    "sexe": "Homme",
    "age": 43,
    "salaire": 1509.56,
    "position": {
        "latitude": -55.5232795,
        "longitude": 157.460948
    }
}
<class 'dict'>
{'firstname': 'Matthieu', 'lastname': 'Lopes', 'sexe': 'Homme', 'age': 43, 'salaire': 1509.56, 'position': {'latitude': -55.5232795, 'longitude': 157.460948}}

--------- Personne 2 ---------
{"firstname": "Constance", "lastname": "Lopez", "sexe": "Femme", "age": 87, "salaire": 2056.35, "position": {"latitude": -56.710608, "longitude": -154.540259}}
{
    "firstname": "Constance",
    "lastname": "Lopez",
    "sexe": "Femme",
    "age": 87,
    "salaire": 2056.35,
    "position": {
        "latitude": -56.710608,
        "longitude": -154.540259
    }
}
<class 'dict'>
{'firstname': 'Constance', 'lastname': 'Lopez', 'sexe': 'Femme', 'age': 87, 'salaire': 2056.35, 'position': {'latitude': -56.710608, 'longitude': -154.540259}}

Le résultat est parfait.

Python: La compréhension de listes et de dictionnaires

La compréhension de listes et de dictionnaires en Python est une méthode très puissante pour transposer un texte structuré en liste, dictionnaire etc etc.

Python: La compréhension de dictionnaires

Nous allons utiliser la même chaine de texte que pour la compréhension de liste.

>>> s = """AIN    1    01    00
AISNE    1    02    00
ALLIER    1    03    00
ALPES(BASSES-)    1    04    00
ALPES(HAUTES-)    1    05    00
ALPES-MARITIMES    1    06    00
ARDECHE    1    07    00
ARDENNES    1    08    00
ARIEGE    1    09    00
AUBE    1    10    00
AUDE    1    11    00
AVEYRON    1    12    00
BOUCHES-DU-RHONE    1    13    00
CALVADOS    1    14    00
CANTAL    1    15    00
CHARENTE    1    16    00
CHARENTE-INFERIEURE    1    17    00
CHER    1    18    00
CORREZE    1    19    00
CORSE    1    20    00
COTE-D'OR    1    21    00
COTES-DU-NORD    1    22    00
CREUSE    1    23    00
"""

Nous allons initialiser un dictionnaire, ayant en clé le nom du département (par exemple), et les données numériques dans une liste correspondante à la valeur de la clé.

>>> d = {x.split('\t')[0]: x.split('\t')[1:] for x in s.split('\n')}
>>> pprint(d)
{'': [],
 'AIN': ['1', '01', '00'],
 'AISNE': ['1', '02', '00'],
 'ALLIER': ['1', '03', '00'],
 'ALPES(BASSES-)': ['1', '04', '00'],
 'ALPES(HAUTES-)': ['1', '05', '00'],
 'ALPES-MARITIMES': ['1', '06', '00'],
 'ARDECHE': ['1', '07', '00'],
 'ARDENNES': ['1', '08', '00'],
 'ARIEGE': ['1', '09', '00'],
 'AUBE': ['1', '10', '00'],
 'AUDE': ['1', '11', '00'],
 'AVEYRON': ['1', '12', '00'],
 'BOUCHES-DU-RHONE': ['1', '13', '00'],
 'CALVADOS': ['1', '14', '00'],
 'CANTAL': ['1', '15', '00'],
 'CHARENTE': ['1', '16', '00'],
 'CHARENTE-INFERIEURE': ['1', '17', '00'],
 'CHER': ['1', '18', '00'],
 'CORREZE': ['1', '19', '00'],
 'CORSE': ['1', '20', '00'],
 "COTE-D'OR": ['1', '21', '00'],
 'COTES-DU-NORD': ['1', '22', '00'],
 'CREUSE': ['1', '23', '00']}

Par contre, notre dictionnaire contient une ligne vide (la première), nous allons donc en tenir compte lors de la création de notre dictionnaire.

>>> d = {x.split('\t')[0]: x.split('\t')[1:] for x in s.split('\n') if x.split('\t')[0] != ''}
>>> pprint(d)
{'AIN': ['1', '01', '00'],
 'AISNE': ['1', '02', '00'],
 'ALLIER': ['1', '03', '00'],
 'ALPES(BASSES-)': ['1', '04', '00'],
 'ALPES(HAUTES-)': ['1', '05', '00'],
 'ALPES-MARITIMES': ['1', '06', '00'],
 'ARDECHE': ['1', '07', '00'],
 'ARDENNES': ['1', '08', '00'],
 'ARIEGE': ['1', '09', '00'],
 'AUBE': ['1', '10', '00'],
 'AUDE': ['1', '11', '00'],
 'AVEYRON': ['1', '12', '00'],
 'BOUCHES-DU-RHONE': ['1', '13', '00'],
 'CALVADOS': ['1', '14', '00'],
 'CANTAL': ['1', '15', '00'],
 'CHARENTE': ['1', '16', '00'],
 'CHARENTE-INFERIEURE': ['1', '17', '00'],
 'CHER': ['1', '18', '00'],
 'CORREZE': ['1', '19', '00'],
 'CORSE': ['1', '20', '00'],
 "COTE-D'OR": ['1', '21', '00'],
 'COTES-DU-NORD': ['1', '22', '00'],
 'CREUSE': ['1', '23', '00']}

Et voilà, tout est nickel. Et tout ça à partir d'une simple chaine de texte.

Pour obtenir les données relatives au département de la Charente:

>>> d['CHARENTE']
['1', '16', '00']
Etiquettes: 

Python: La compréhension de listes

Par exemple, avec la chaine de texte suivante qui contient des noms de départements et quelques données numériques:

>>> s = """AIN    1    01    00
AISNE    1    02    00
ALLIER    1    03    00
ALPES(BASSES-)    1    04    00
ALPES(HAUTES-)    1    05    00
ALPES-MARITIMES    1    06    00
ARDECHE    1    07    00
ARDENNES    1    08    00
ARIEGE    1    09    00
AUBE    1    10    00
AUDE    1    11    00
AVEYRON    1    12    00
BOUCHES-DU-RHONE    1    13    00
CALVADOS    1    14    00
CANTAL    1    15    00
CHARENTE    1    16    00
CHARENTE-INFERIEURE    1    17    00
CHER    1    18    00
CORREZE    1    19    00
CORSE    1    20    00
COTE-D'OR    1    21    00
COTES-DU-NORD    1    22    00
CREUSE    1    23    00
"""

Quand on l'affiche dans une console Python:

>>> s.__str__()
"AIN\t1\t01\t00\nAISNE\t1\t02\t00\nALLIER\t1\t03\t00\nALPES(BASSES-)\t1\t04\t00\nALPES(HAUTES-)\t1\t05\t00\nALPES-MARITIMES\t1\t06\t00\nARDECHE\t1\t07\t00\nARDENNES\t1\t08\t00\nARIEGE\t1\t09\t00\nAUBE\t1\t10\t00\nAUDE\t1\t11\t00\nAVEYRON\t1\t12\t00\nBOUCHES-DU-RHONE\t1\t13\t00\nCALVADOS\t1\t14\t00\nCANTAL\t1\t15\t00\nCHARENTE\t1\t16\t00\nCHARENTE-INFERIEURE\t1\t17\t00\nCHER\t1\t18\t00\nCORREZE\t1\t19\t00\nCORSE\t1\t20\t00\nCOTE-D'OR\t1\t21\t00\nCOTES-DU-NORD\t1\t22\t00\nCREUSE\t1\t23\t00\n"

On s'aperçoit que la chaine est composée de lignes séparées par '\n' et des colonnes séparées par '\t'.

Pour convertir cette chaine de texte en une liste simple il suffit de faire:

>>> l = s.split('\n')
>>> print(l)
['AIN\t1\t01\t00', 'AISNE\t1\t02\t00', 'ALLIER\t1\t03\t00', 'ALPES(BASSES-)\t1\t04\t00', 'ALPES(HAUTES-)\t1\t05\t00', 'ALPES-MARITIMES\t1\t06\t00', 'ARDECHE\t1\t07\t00', 'ARDENNES\t1\t08\t00', 'ARIEGE\t1\t09\t00', 'AUBE\t1\t10\t00', 'AUDE\t1\t11\t00', 'AVEYRON\t1\t12\t00', 'BOUCHES-DU-RHONE\t1\t13\t00', 'CALVADOS\t1\t14\t00', 'CANTAL\t1\t15\t00', 'CHARENTE\t1\t16\t00', 'CHARENTE-INFERIEURE\t1\t17\t00', 'CHER\t1\t18\t00', 'CORREZE\t1\t19\t00', 'CORSE\t1\t20\t00', "COTE-D'OR\t1\t21\t00", 'COTES-DU-NORD\t1\t22\t00', 'CREUSE\t1\t23\t00', '']

C'est mieux car on peut maintenant parcourir la liste ligne par ligne mais ce qui suit est encore mieux:

>>> l = [x.split('\t') for x in s.split('\n')]
>>> print(l)
[['AIN', '1', '01', '00'], ['AISNE', '1', '02', '00'], ['ALLIER', '1', '03', '00'], ['ALPES(BASSES-)', '1', '04', '00'], ['ALPES(HAUTES-)', '1', '05', '00'], ['ALPES-MARITIMES', '1', '06', '00'], ['ARDECHE', '1', '07', '00'], ['ARDENNES', '1', '08', '00'], ['ARIEGE', '1', '09', '00'], ['AUBE', '1', '10', '00'], ['AUDE', '1', '11', '00'], ['AVEYRON', '1', '12', '00'], ['BOUCHES-DU-RHONE', '1', '13', '00'], ['CALVADOS', '1', '14', '00'], ['CANTAL', '1', '15', '00'], ['CHARENTE', '1', '16', '00'], ['CHARENTE-INFERIEURE', '1', '17', '00'], ['CHER', '1', '18', '00'], ['CORREZE', '1', '19', '00'], ['CORSE', '1', '20', '00'], ["COTE-D'OR", '1', '21', '00'], ['COTES-DU-NORD', '1', '22', '00'], ['CREUSE', '1', '23', '00'], ['']]

Chaque colonne est séparée dans une liste. On appelle ça une compréhension de liste.

Il est donc très facile d'accéder à une donnée en particulier:

>>> print(l[0][0])
AIN
Etiquettes: 

Python: La compréhension de listes, effectuer des opérations complexes

Voici un exemple, qui ne sert pas à grand chose, mais qui permet de montrer les différents calculs complexes qu'il est possible de faire avec la compréhension de liste.

Dans cet exemple, j'ai une classe qui permet de générer, aléatoirement, des codes EAN13.

>>> from random import randint
>>> class EAN13():
    GENERATES = []
    def __init__(self):
        pass
    def new(self, count=1):
        ct = count
        while ct > 0:
            ean13 = '{:03}{}'.format(randint(40, 49), ''.join([((x+4)*'0'+str(randint(1, int((x+4)*'9'))))[-(x+4):] for x in range(2)]))
            ean13 += str(10 - (sum([int(y) * 3 if x % 2 == 0 else int(y) for x, y in enumerate(list(ean13), start=1)]) % 10))[-1]
            if ean13 not in EAN13.GENERATES:
                EAN13.GENERATES.append(ean13)
                ct -= 1
        return EAN13.GENERATES[-count:]

>>> EAN13().new(count=20)
['0467764013590', '0442457715586', '0478050754264', '0498544873950', '0419792606732', '0436177825380', '0487308428246', '0449011102394', '0487403914866', '0462030759684', '0438507088359', '0405068027851', '0489779856153', '0437057321695', '0465499916742', '0467772469648', '0493671951373', '0483943989975', '0499830229161', '0462787735498']

Comme indiqué sur wikipédia:

Un code EAN13 est composé de 13 chiffres

  • les deux ou trois premiers correspondent au pays de provenance du produit, ou à une classe normalisée de produits ;
  • les 4 ou 5 suivants sont le numéro de membre de l’entreprise participant au système EAN ;
  • les 4 ou 5 suivants sont le numéro d’article du produit ainsi marqué et
  • le treizième est une clé de contrôle calculée en fonction des douze précédents.

Je vais "exploser" mon code pour expliquer les différentes étapes.

Voici la ligne qui permet de générer aléatoirement les 12 premiers chiffres:

>>> ean13 = '{:03}{}'.format(randint(40, 49), ''.join([((x+4)*'0'+str(randint(1, int((x+4)*'9'))))[-(x+4):] for x in range(2)]))

Les 3 premiers chiffres de mon code, ceux correspondant au pays de provenance du produit, ou à une classe normalisée de produits, est un nombre aléatoire allant de 040 à 049 (à l'aide la fonction randint et format)

>>> '{:03}{}'.format(randint(40, 49), '')
'041'

Voici la fameuse compréhension de liste qui va permettre de générer deux nombres.
Le premier composé de 4 chiffres et le second composé de 5 chiffres.

>>> [((x+4)*'0'+str(randint(1, int((x+4)*'9'))))[-(x+4):] for x in range(2)]
['2983', '23696']

Si nous faisions la même chose mais sans utiliser la compréhension de liste, ça donnerait ceci:

>>> L = []
>>> for x in range(2):
    L.append(((x+4)*'0'+str(randint(1, int((x+4)*'9'))))[-(x+4):])
   
>>> L
['5237', '92948']

J'utilise donc ma boucle for pour gérérer la première fois (x=0) un nombre de 4 chiffres et la fois suivante (x=1) un nombre de 5 chiffres.

J'utilise également le slicing ([-(x+4):]) pour conserver uniquement les x derniers chiffres de mes deux nombres aléatoires auquels j'ai ajoutés des '0' à gauche pour être certain d'avoir le bon nombre de chiffres.

J'aurais également pû utiliser la fonction format comme ceci:

>>> L.append('{0:0{1}}'.format(randint(1, int((x+4)*'9')), x+4))

Il ne reste plus qu'à calculer la clé qui sera donc le treizième et dernier chiffre de notre code.

Voici donc la ligne de code qui permet de le faire:

>>> ean13 += str(10 - (sum([int(y) * 3 if x % 2 == 0 else int(y) for x, y in enumerate(list(ean13), start=1)]) % 10))[-1]

Cette ligne de code utilise également la compréhension de liste.

J'utilise donc une boucle for et la fonction enumerate qui permet d'indexer chaque chiffres de mon code.

Je vais donc pouvoir faire la somme de tous mes chiffres et en ayant multiplié par 3 les rangs pairs (comme indiqué dans la formule de calcul de la clé).

>>> list(ean13)
['0', '4', '6', '5', '0', '3', '9', '9', '9', '0', '9', '9']
>>> [int(y) * 3 if x % 2 == 0 else int(y) for x, y in enumerate(list(ean13), start=1)]
[0, 12, 6, 15, 0, 9, 9, 27, 9, 0, 9, 27]
>>> sum([int(y) * 3 if x % 2 == 0 else int(y) for x, y in enumerate(list(ean13), start=1)])
123

Ci-dessous le reste du code détaillé ligne par ligne pour obtenir la clé finale (le caractère '_' représentant le résultat de la dernière exécution)

>>> _ % 10
3
>>> 10 - _
7
>>> str(_)[-1]
'7'

Pour info:
La première ligne permet d'obtenir le reste de la division par 10 (123%10=3)
La seconde ligne permet de soustraire à 10 le chiffre précédement obtenu.
La troisième ligne permet uniquement de garder le bon chiffre, dans le cas où le reste de la division est égal à 0.

La compréhension de list en Python est vraiment très puissante.
Elle permet de faire beaucoup de choses d'une manière plus concentrée et parfois plus facile à comprendre.

J'espère avoir été assez clair dans mes explications...

Etiquettes: 

Python: Le module CSV

Testé avec Python 3.5

Le module csv de python permet de lire et d'écrire des fichiers csv très facilement.

Créer un fichier csv:

Exemple avec la liste suivante (cette liste contient 13 sous-listes de 7 valeurs)

>>> pprint(l)
[['root', 'x', '0', '0', 'root', '/root', '/bin/bash'],
 ['daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin'],
 ['bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin'],
 ['sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin'],
 ['sync', 'x', '4', '65534', 'sync', '/bin', '/bin/sync'],
 ['games', 'x', '5', '60', 'games', '/usr/games', '/usr/sbin/nologin'],
 ['man', 'x', '6', '12', 'man', '/var/cache/man', '/usr/sbin/nologin'],
 ['lp', 'x', '7', '7', 'lp', '/var/spool/lpd', '/usr/sbin/nologin'],
 ['mail', 'x', '8', '8', 'mail', '/var/mail', '/usr/sbin/nologin'],
 ['news', 'x', '9', '9', 'news', '/var/spool/news', '/usr/sbin/nologin'],
 ['uucp', 'x', '10', '10', 'uucp', '/var/spool/uucp', '/usr/sbin/nologin'],
 ['proxy', 'x', '13', '13', 'proxy', '/bin', '/usr/sbin/nologin'],
 ['www-data', 'x', '33', '33', 'www-data', '/var/www', '/usr/sbin/nologin']]

>>> import csv
>>> with open('passwd.csv', 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(l)

Vachement simple, non !

infoLa fonction "writerows" avec un "s" à la fin, permet d'écrire en une seule fois tout le contenu d'une liste contenant elle-même des sous-listes (comme dans l'exemple ci-dessus).
Par contre, pour écrire uniquement le contenu d'une liste, sans sous-liste, il faut utiliser la fonction "writerow" (sans le "s" à la fin).

>>> import csv
>>> with open('passwd.csv', 'w', newline='') as f:
        writer = csv.writer(f)
        for x in l:
            writer.writerow(x)

warning Lors de l'ouverture du fichier à l'aide de la commande open, il faut obligatoirement indiqué le paramètre newline='' sinon des sauts de lignes supplémentaires seront ajoutés à chaque écriture.

Lire un fichier csv:

>>> import csv
>>> with open('passwd.csv', newline='') as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

        
['root', 'x', '0', '0', 'root', '/root', '/bin/bash']
['daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin']
['bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin']
['sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin']
['sync', 'x', '4', '65534', 'sync', '/bin', '/bin/sync']
['games', 'x', '5', '60', 'games', '/usr/games', '/usr/sbin/nologin']
['man', 'x', '6', '12', 'man', '/var/cache/man', '/usr/sbin/nologin']
['lp', 'x', '7', '7', 'lp', '/var/spool/lpd', '/usr/sbin/nologin']
['mail', 'x', '8', '8', 'mail', '/var/mail', '/usr/sbin/nologin']
['news', 'x', '9', '9', 'news', '/var/spool/news', '/usr/sbin/nologin']
['uucp', 'x', '10', '10', 'uucp', '/var/spool/uucp', '/usr/sbin/nologin']
['proxy', 'x', '13', '13', 'proxy', '/bin', '/usr/sbin/nologin']
['www-data', 'x', '33', '33', 'www-data', '/var/www', '/usr/sbin/nologin']

Toujours aussi simple !

warning Ne pas oublier le paramètre newline='' avec la commande open

En prime, lecture d'un fichier csv à l'aide d'un tuple nommé contenant le nom des champs du fichiers csv.
Très utile pour la manipulation d'un fichier csv complexe.

Pour cela, nous allons utiliser la fonction namedtuple du module collections

>>> from collections import namedtuple
>>> # Nous initialisons la liste des noms de champs
>>> Headers = namedtuple('Headers', 'LoginName, EncryptedPassword, UserId, GroupId, UserName, HomeDirectory, Interpreter')
>>> with open('passwd.csv', newline='') as f:
    reader = csv.reader(f)
    for header in map(Headers._make, reader):
        # Nous pouvons afficher les valeurs à l'aide des attributs nommés
        print(header.LoginName, header.HomeDirectory)

        
root /root
daemon /usr/sbin
bin /bin
sys /dev
sync /bin
games /usr/games
man /var/cache/man
lp /var/spool/lpd
mail /var/mail
news /var/spool/news
uucp /var/spool/uucp
proxy /bin
www-data /var/www

Que dire de plus ...

Python: Le module enum

Le module enum est très pratique pour créer et utiliser des constantes dans des programmes python.

Dans l'exemple suivant, je crée une classe qui va me permettre de générer des constantes pour les codes HTTP à l'aide de la classe Enum du module enum.

Cette classe pourra ensuite être utilisée dans n'importe quel programme ayant besoin d'utiliser ces codes.

Pour l'exemple, j'ai utilisé que quelques codes.
La liste complète étant disponible ici.

from enum import Enum

class Status(Enum):
    HTTP_OK = 200
    HTTP_CREATED = 201
    HTTP_ACCEPTED = 202
    HTTP_NON_AUTHORITATIVE = 203
    HTTP_NO_CONTENT = 204
    HTTP_PARTIAL_CONTENT = 206
    HTTP_MULTIPLE_CHOICES = 300
    HTTP_MOVED_PERMANENTLY = 301
    HTTP_MOVED_TEMPORARILY = 302
    HTTP_SEE_OTHER = 303
    HTTP_NOT_MODIFIED = 304
    HTTP_BAD_REQUEST = 400
    HTTP_UNAUTHORIZED = 401
    HTTP_PAYMENT_REQUIRED = 402
    HTTP_FORBIDDEN = 403
    HTTP_NOT_FOUND = 404
    HTTP_METHOD_NOT_ALLOWED = 405
    HTTP_INTERNAL_SERVER_ERROR = 500
    HTTP_NOT_IMPLEMENTED = 501
    HTTP_BAD_GATEWAY = 502

J'ai donc créé une classe Status qui étend la classe Enum du module enum.

Pour créer les constantes, il suffit juste de créer des paires clés / valeurs.
Les valeurs peuvent être numériques ou des chaines de caractères.

Pour utiliser cette classe personnalisée, il suffit de procéder de cette manière.

Pour l'exemple, je pars du principe que la classe Status est enregistrée dans un fichier nommé Apache.py et que ce fichier est disponible dans un répertoire listé dans la variable path du module sys.

>>> from Apache import Status

>>> Status
<enum 'Status'>
>>> Status(200)
<Status.HTTP_OK: 200>
>>> Status['HTTP_UNAUTHORIZED']
<Status.HTTP_UNAUTHORIZED: 401>
>>> Status.HTTP_BAD_REQUEST
<Status.HTTP_BAD_REQUEST: 400>
>>> Status(200).value
200
>>> Status(200).name
'HTTP_OK'
>>> Status['HTTP_OK'].value
200

On voit bien que la classe Status étend le module enum.
Pour afficher la description (le nom de la constante correspondante) d'un code, il suffit d'appeler la classe avec le code entre parenthèses.
Si le code n'existe pas, une exception "ValueError" est levée.
On peut également afficher l'information à l'aide du nom de la constante entre crochets (comme un dictionnaire).
Les constantes sont accessibles directement via la classe Status comme n'importe quelles constantes de classes ordinaires.
Il est également très facile d'obtenir la valeur d'une constante et/ou son nom que ce soit à partir de sa valeur ou de son nom.

Il est également possible d'ajouter des informations supplémentaires comme un label par exemple.

from enum import Enum


class Status(int, Enum):

    HTTP_OK = (200, 'Ok')
    HTTP_CREATED = (201, 'Created')
    HTTP_ACCEPTED = (202, 'Accepted')
    HTTP_NON_AUTHORITATIVE = (203, 'Non Authoritative')
    HTTP_NO_CONTENT = (204, 'No Content')
    HTTP_PARTIAL_CONTENT = (206, 'Partial Content')
    HTTP_MULTIPLE_CHOICES = (300, 'Multiple Choices')
    HTTP_MOVED_PERMANENTLY = (301, 'Moved Permanently')
    HTTP_MOVED_TEMPORARILY = (302, 'Moved Temporarily')
    HTTP_SEE_OTHER = (303, 'See Other')
    HTTP_NOT_MODIFIED = (304, 'Not Modified')
    HTTP_BAD_REQUEST = (400, 'Bad Request')
    HTTP_UNAUTHORIZED = (401, 'Unauthorized')
    HTTP_PAYMENT_REQUIRED = (402, 'Payment Required')
    HTTP_FORBIDDEN = (403, 'Forbidden')
    HTTP_NOT_FOUND = (404, 'Not Found')
    HTTP_METHOD_NOT_ALLOWED = (405, 'Method Not Allowed')
    HTTP_INTERNAL_SERVER_ERROR = (500, 'Internal Server Error')
    HTTP_NOT_IMPLEMENTED = (501, 'Not Implemented')
    HTTP_BAD_GATEWAY = (502, 'Bad Gateway')

    def __new__(cls, value, label):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj.label = label
        return obj

Pour ajouter des labels par exemple, il est nécessaire de modifier la classe Status.
Il faut ajouter une dépendance à l'objet int.
Il faut ensuite modifier les valeurs en les remplaçant par des tuples contenant la valeur et le label.
Pour finir, il faut ajouter une fonction __new__ afin d'indiquer quelle valeur du tuple sera affectée à l'attribut _value_
La nouvelle classe est sauvegardée dans un fichier nommé HTTP_Status.py

Résultat:

from HTTP_Status import Status
Status
Out[3]: <enum 'Status'>
Status.HTTP_OK
Out[4]: <Status.HTTP_OK: 200>
Status.HTTP_OK.name
Out[5]: 'HTTP_OK'
Status.HTTP_OK.value
Out[6]: 200
Status.HTTP_OK.label
Out[7]: 'Ok'
Status.HTTP_NOT_FOUND
Out[8]: <Status.HTTP_NOT_FOUND: 404>
Status.HTTP_NOT_FOUND.name
Out[9]: 'HTTP_NOT_FOUND'
Status.HTTP_NOT_FOUND.value
Out[10]: 404
Status.HTTP_NOT_FOUND.label
Out[11]: 'Not Found'
Status(404)
Out[12]: <Status.HTTP_NOT_FOUND: 404>
Status(404).label
Out[13]: 'Not Found'
Status['HTTP_NOT_FOUND']
Out[14]: <Status.HTTP_NOT_FOUND: 404>
Status['HTTP_NOT_FOUND'].label
Out[15]: 'Not Found'
Status['HTTP_NOT_FOUND'].value
Out[16]: 404

 

Etiquettes: 

Python: Le module json

Sous Python, le module json permet de créer et de lire des données au format json.

Repartons de cet exemple: Python: Parser et indenter un flux XML

J'ai donc un dictionnaire de données contenant les valeurs suivantes:

>>> from pprint import pprint
>>> pprint(datas)
{'2782113': {'countryCode': 'AT',
             'countryName': 'Austria',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.33333',
             'lng': '13.33333',
             'name': 'Austria',
             'toponymName': 'Republic of Austria'},
 '2921044': {'countryCode': 'DE',
             'countryName': 'Germany',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '51.5',
             'lng': '10.5',
             'name': 'Germany',
             'toponymName': 'Federal Republic of Germany'},
 '3017382': {'countryCode': 'FR',
             'countryName': 'France',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '46',
             'lng': '2',
             'name': 'France',
             'toponymName': 'Republic of France'},
 '3042058': {'countryCode': 'LI',
             'countryName': 'Liechtenstein',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.16667',
             'lng': '9.53333',
             'name': 'Liechtenstein',
             'toponymName': 'Principality of Liechtenstein'},
 '3175395': {'countryCode': 'IT',
             'countryName': 'Italy',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '42.83333',
             'lng': '12.83333',
             'name': 'Italy',
             'toponymName': 'Repubblica Italiana'}}
>>>

Pour convertir ce dictionnaire au format json, il suffit d'utiliser le module json de Python:

>>> import json
>>> print(json.dumps(datas, indent=4))
{
    "3017382": {
        "toponymName": "Republic of France",
        "lat": "46",
        "countryName": "France",
        "fcl": "A",
        "fcode": "PCLI",
        "name": "France",
        "countryCode": "FR",
        "lng": "2"
    },
    "3175395": {
        "toponymName": "Repubblica Italiana",
        "lat": "42.83333",
        "countryName": "Italy",
        "fcl": "A",
        "fcode": "PCLI",
        "name": "Italy",
        "countryCode": "IT",
        "lng": "12.83333"
    },
    "2921044": {
        "toponymName": "Federal Republic of Germany",
        "lat": "51.5",
        "countryName": "Germany",
        "fcl": "A",
        "fcode": "PCLI",
        "name": "Germany",
        "countryCode": "DE",
        "lng": "10.5"
    },
    "3042058": {
        "toponymName": "Principality of Liechtenstein",
        "lat": "47.16667",
        "countryName": "Liechtenstein",
        "fcl": "A",
        "fcode": "PCLI",
        "name": "Liechtenstein",
        "countryCode": "LI",
        "lng": "9.53333"
    },
    "2782113": {
        "toponymName": "Republic of Austria",
        "lat": "47.33333",
        "countryName": "Austria",
        "fcl": "A",
        "fcode": "PCLI",
        "name": "Austria",
        "countryCode": "AT",
        "lng": "13.33333"
    }
}
>>>

Dans la commande précédente, j'ai utiliser la fonction dumps du module json qui permet de formater n'importe quel objet Python (dictionnaire, liste, une chaine de caractères, un nombre) au format json avec en plus une indentation avec 4 espaces (indent=4).

Il est donc tout à fait possible d'enregistrer ce résultat dans un fichier:

>>> with open('datas.json', 'w') as f:
    f.write(json.dumps(datas, indent=4))

    
1293
>>>

Ou alors avec la fonction dump (sans le `s` à la fin).
Cette commande permet d'enregistrer les données directement dans le flux précédemment ouvert.

>>> with open('datas.json', 'w') as f:
    json.dump(datas, f, indent=4)

    
>>>

Et voilà, je me retrouve avec un beau fichier json tout beau tout neuf.

Et pour lire un contenu json:

>>> with open('datas.json', 'r') as f:
    datas = json.load(f)

    
>>> type(datas)
<class 'dict'>
>>> pprint(datas)
{'2782113': {'countryCode': 'AT',
             'countryName': 'Austria',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.33333',
             'lng': '13.33333',
             'name': 'Austria',
             'toponymName': 'Republic of Austria'},
 '2921044': {'countryCode': 'DE',
             'countryName': 'Germany',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '51.5',
             'lng': '10.5',
             'name': 'Germany',
             'toponymName': 'Federal Republic of Germany'},
 '3017382': {'countryCode': 'FR',
             'countryName': 'France',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '46',
             'lng': '2',
             'name': 'France',
             'toponymName': 'Republic of France'},
 '3042058': {'countryCode': 'LI',
             'countryName': 'Liechtenstein',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.16667',
             'lng': '9.53333',
             'name': 'Liechtenstein',
             'toponymName': 'Principality of Liechtenstein'},
 '3175395': {'countryCode': 'IT',
             'countryName': 'Italy',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '42.83333',
             'lng': '12.83333',
             'name': 'Italy',
             'toponymName': 'Repubblica Italiana'}}
>>>

Grâce à la fonction load du module json, je peux charger tout le contenu d'un fichier json directement dans une variable.

Cette variable est de type dict (dictionnaire)

La fonction loads (avec un `s` à la fin) permet de charger une chaine de caractères au format json.

Etiquettes: 

Python: Le module pickle

Comme son nom l'indique, enfin non, pas vraiment, le module pickle permet de sauvegarder dans un fichier, au format binaire,  n'importe quel objet Python.

En clair, si pour une raison quelconque, dans un script Python, vous avez besoin de sauvegarder, temporairement ou même de façon plus pérenne, le contenu d'un objet Python comme une liste, un dictionnaire, un tuple etc etc ... au lieu d'utiliser une base de données ou un simple fichier texte, le module pickle est fait pour ça.

Il permet de stocker et de restaurer un objet Python tel quel sans aucune manipulation supplémentaire.

C'est vraiment super pratique.

Il fonctionne comme le module json mais n'est pas limité à un seul format d'objet.

Exemples:

>>> import pickle
>>> import string
>>> L = list(string.ascii_letters)
>>> print(L)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
>>> with open('mypicklefile', 'wb') as f1:
    pickle.dump(L, f1)
>>> with open('mypicklefile', 'r') as f1:
    f1.read()

    
'€\x03]q\x00(X\x01\x00\x00\x00aq\x01X\x01\x00\x00\x00bq\x02X\x01\x00\x00\x00cq\x03X\x01\x00\x00\x00dq\x04X\x01\x00\x00\x00eq\x05X\x01\x00\x00\x00fq\x06X\x01\x00\x00\x00gq\x07X\x01\x00\x00\x00hq\x08X\x01\x00\x00\x00iq\tX\x01\x00\x00\x00jq\nX\x01\x00\x00\x00kq\x0bX\x01\x00\x00\x00lq\x0cX\x01\x00\x00\x00mq\nX\x01\x00\x00\x00nq\x0eX\x01\x00\x00\x00oq\x0fX\x01\x00\x00\x00pq\x10X\x01\x00\x00\x00qq\x11X\x01\x00\x00\x00rq\x12X\x01\x00\x00\x00sq\x13X\x01\x00\x00\x00tq\x14X\x01\x00\x00\x00uq\x15X\x01\x00\x00\x00vq\x16X\x01\x00\x00\x00wq\x17X\x01\x00\x00\x00xq\x18X\x01\x00\x00\x00yq\x19X\x01\x00\x00\x00zq\x1aX\x01\x00\x00\x00Aq\x1bX\x01\x00\x00\x00Bq\x1cX\x01\x00\x00\x00Cq\x1dX\x01\x00\x00\x00Dq\x1eX\x01\x00\x00\x00Eq\x1fX\x01\x00\x00\x00Fq X\x01\x00\x00\x00Gq!X\x01\x00\x00\x00Hq"X\x01\x00\x00\x00Iq#X\x01\x00\x00\x00Jq$X\x01\x00\x00\x00Kq%X\x01\x00\x00\x00Lq&X\x01\x00\x00\x00Mq\'X\x01\x00\x00\x00Nq(X\x01\x00\x00\x00Oq)X\x01\x00\x00\x00Pq*X\x01\x00\x00\x00Qq+X\x01\x00\x00\x00Rq,X\x01\x00\x00\x00Sq-X\x01\x00\x00\x00Tq.X\x01\x00\x00\x00Uq/X\x01\x00\x00\x00Vq0X\x01\x00\x00\x00Wq1X\x01\x00\x00\x00Xq2X\x01\x00\x00\x00Yq3X\x01\x00\x00\x00Zq4e.'
>>> OL = None
>>> print(OL)
None
>>> with open('mypicklefile', 'rb') as f1:
    OL = pickle.load(f1)
>>> type(OL)
<class 'list'>
>>> print(OL)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
>>> L == OL
True
>>>

Dans l'exemple ci-dessus, j'ai créé une liste "L" contenant toutes les lettres de l'alphabet.
J'ai sauvegardé mon objet "L" (liste Python) dans un fichier "mypicklefile" grâce à la méthode dump du module pickle.
Précision importante, le module pickle écrit les données uniquement dans un fichier ouvert en mode binaire.
J'ai ouvert le fichier et afficher son contenu pour bien montrer que pickle écrit les données au format binaire.
J'ai créé un nouvel objet "OL" ayant None comme valeur (cette étape n'est pas obligatoire - uniquement pour montrer que l'objet "OL" n'existait pas auparavant).
J'ai ensuite chargé le contenu du fichier "mypicklefile" dans mon objet "OL" grâce à la méthode load du module pickle.
J'affiche le contenu de la nouvelle liste "OL" et le test d'égalité de l'objet "L" et "OL" pour bien montrer que les deux objets sont bien identiques.

La liste "OL" peut être modifiée (ajout, modification, suppression des valeurs) et à nouveau sauvegardée dans le fichier grâce à la méthode dump du module pickle pour une prochaine utilisation.

Petite précision, pour la méthode dump, le fichier doit être ouvert en mode 'wb' afin d'écraser le contenu précédent.
Si le fichier est ouvert en mode 'ab', les données sont écrites à la fin du fichier mais la méthode load récupère les données au début du fichier.
De toute manière, le mode append n'a aucun intérêt pour ce genre de stockage de données. On dump et on load l'intégralité du contenu d'un objet.

Et ça fonctionne pour tous types d'objets

Avec un tuple

>>> import pickle
>>> T = tuple(string.ascii_letters)
>>> T
('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
>>> with open('mypicklefile', 'wb') as f1:
    pickle.dump(T, f1)
>>> with open('mypicklefile', 'rb') as f1:
    OT = pickle.load(f1)
>>> OT
('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
>>>

Avec un dictionnaire

>>> import pickle
>>> I = list(range(52))
>>> L = list(string.ascii_letters)
>>> D = {i: l for i, l in zip(I, L)}
>>> D
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h', 8: 'i', 9: 'j', 10: 'k', 11: 'l', 12: 'm', 13: 'n', 14: 'o', 15: 'p', 16: 'q', 17: 'r', 18: 's', 19: 't', 20: 'u', 21: 'v', 22: 'w', 23: 'x', 24: 'y', 25: 'z', 26: 'A', 27: 'B', 28: 'C', 29: 'D', 30: 'E', 31: 'F', 32: 'G', 33: 'H', 34: 'I', 35: 'J', 36: 'K', 37: 'L', 38: 'M', 39: 'N', 40: 'O', 41: 'P', 42: 'Q', 43: 'R', 44: 'S', 45: 'T', 46: 'U', 47: 'V', 48: 'W', 49: 'X', 50: 'Y', 51: 'Z'}
>>> with open('mypicklefile', 'wb') as f1:
    pickle.dump(D, f1)
>>> with open('mypicklefile', 'rb') as f1:
    OD = pickle.load(f1)
>>> OD
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e', 5: 'f', 6: 'g', 7: 'h', 8: 'i', 9: 'j', 10: 'k', 11: 'l', 12: 'm', 13: 'n', 14: 'o', 15: 'p', 16: 'q', 17: 'r', 18: 's', 19: 't', 20: 'u', 21: 'v', 22: 'w', 23: 'x', 24: 'y', 25: 'z', 26: 'A', 27: 'B', 28: 'C', 29: 'D', 30: 'E', 31: 'F', 32: 'G', 33: 'H', 34: 'I', 35: 'J', 36: 'K', 37: 'L', 38: 'M', 39: 'N', 40: 'O', 41: 'P', 42: 'Q', 43: 'R', 44: 'S', 45: 'T', 46: 'U', 47: 'V', 48: 'W', 49: 'X', 50: 'Y', 51: 'Z'}
>>>

Avec un deque (du module collections)

>>> import pickle
>>> from collections import deque
>>> L = list(string.ascii_letters)
>>> L
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
>>> DE = deque(L)
>>> DE
deque(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'])
>>> with open('mypicklefile', 'wb') as f1:
    pickle.dump(DE, f1)
>>> with open('mypicklefile', 'rb') as f1:
    ODE = pickle.load(f1)
>>> ODE
deque(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'])
>>>

Précision importante, la méthode load importe automatiquement l'objet nécessaire au chargement des données.
C'est à dire que dans l'exemple ci-dessus, l'objet deque du module collections n'a pas besoin d'être importé pour être correctement chargé via la méthode load. Il doit bien évidement être disponible dans la liste des modules de Python.

Et enfin, avec un objet perso

>>> class MyObject():
    def __init__(self):
        self.un = 1
        self.deux = 2
        self.trois = 3
    def __repr__(self):
        return "{}, {}, {}".format(self.un, self.deux, self.trois)
    def __str__(self):
        return "un: {}\ndeux: {}\ntrois: {}".format(self.un, self.deux, self.trois)

    
>>> A = MyObject()
>>> A
1, 2, 3
>>> print(A)
un: 1
deux: 2
trois: 3
>>> with open('mypicklefile', 'wb') as f1:
    pickle.dump(A, f1)
>>> with open('mypicklefile', 'rb') as f1:
    B = pickle.load(f1)
>>> B
1, 2, 3
>>> print(B)
un: 1
deux: 2
trois: 3
>>>

Dans ce cas, la class MyObject() doit être disponible pour pouvoir être utilisée via la méthode load sinon, une erreur "AttributeError" est levée.

Associé au module tempfile, nous avons un système complet de sauvegarde temporaire d'objets utilisable dans n'importe quel script Python.

Etiquettes: 

Python: Le module requests

Aujourd'hui, le web a prit une place importante dans notre vie de tous les jours.

Que ce soit pour lire ses mails, lire les actualités, consulter la météo, se documenter, jouer, bref la liste est longue et non exhaustive.

Python permet de faire différents types de requêtes sur le web.
Pour ce faire, Python propose le module urllib3.
Ce module est très complet et par conséquent un peu complexe à utiliser.

Le module requests, qui utilise toute la puissance du module urllib3 est beaucoup plus simple d'utilisation et énormément utilisé pour interagir avec le web.

Comme un petit exemple vaut mieux qu'un long discours...

>>> import requests
>>> html = requests.get('https://www.python.org/downloads/')
>>> html.status_code
200
>>> html.url
'https://www.python.org/downloads/'
>>> html.raw
<urllib3.response.HTTPResponse object at 0x0B80D6D0>
>>> html.encoding
'utf-8'
>>> html.headers
{'Server': 'nginx', 'Content-Type': 'text/html; charset=utf-8', 'X-Frame-Options': 'DENY', 'Cache-Control': 'max-age=604800, public', 'Via': '1.1 vegur, 1.1 varnish, 1.1 varnish', 'Content-Length': '113054', 'Accept-Ranges': 'bytes', 'Date': 'Thu, 06 Jun 2019 13:08:10 GMT', 'Age': '132076', 'Connection': 'keep-alive', 'X-Served-By': 'cache-iad2130-IAD, cache-cdg20735-CDG', 'X-Cache': 'HIT, HIT', 'X-Cache-Hits': '1, 5', 'X-Timer': 'S1559826490.039199,VS0,VE1', 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains'}
>>> html.is_redirect
False

Voici donc un exemple d'une requête "GET" et des différentes informations que nous obtenons (dans l'ordre):
- Le statut de la requête (ici 200, indiquant que la requête s'est bien déroulée)
- L'url requêtée
- Le format brut de notre requête (on voit qu'il s'agit d'un objet HTTPResponse du module urllib3)
- De l'encodage utilisé (ici, UTF-8)
- Tout le contenu de l'en-tête
- Si la requête effectuée est une redirection

Mais une des choses les plus importantes est quand même le contenu de la requête....

>>> len(html.content)
113054
>>> html.content[:500]
b'<!doctype html>\n<!--[if lt IE 7]>   <html class="no-js ie6 lt-ie7 lt-ie8 lt-ie9">   <![endif]-->\n<!--[if IE 7]>      <html class="no-js ie7 lt-ie8 lt-ie9">          <![endif]-->\n<!--[if IE 8]>      <html class="no-js ie8 lt-ie9">                 <![endif]-->\n<!--[if gt IE 8]><!--><html class="no-js" lang="en" dir="ltr">  <!--<![endif]-->\n\n<head>\n    <meta charset="utf-8">\n    <meta http-equiv="X-UA-Compatible" content="IE=edge">\n\n    <link rel="prefetch" href="//ajax.googleapis.com/ajax/libs/jqu'
>>> html.content[-500:]
b'ncludes.js"></script>\n\n    <script type="text/javascript" src="/static/js/main-min.fbfe252506ae.js" charset="utf-8"></script>\n    \n\n    <!--[if lte IE 7]>\n    <script type="text/javascript" src="/static/js/plugins/IE8-min.16868e6a5d2f.js" charset="utf-8"></script>\n    \n    \n    <![endif]-->\n\n    <!--[if lte IE 8]>\n    <script type="text/javascript" src="/static/js/plugins/getComputedStyle-min.c3860be1d290.js" charset="utf-8"></script>\n    \n    \n    <![endif]-->\n\n    \n\n    \n    \n\n</body>\n</html>\n'

J'affiche les 500 premiers/derniers caractères du contenu de la requête qui en contient au total 113054.

Nous voilà donc avec tout le contenu de la page https://www.python.org/downloads/.

Si les données de la requête sont au format JSON, très courant dans le monde des APIs et des WebServices.

L'objet retourné par request contient une méthode json qui permet de formater le contenu en JSON si celui-ci est reconnu comme tel.

Par exemple, avec la liste des codes postaux...

>>> req = requests.get('https://unpkg.com/codes-postaux@3.2.0/codes-postaux.json')
>>> req.status_code
200
>>> json = req.json()
>>> type(json)
<class 'list'>
>>> len(json)
35728
>>> pprint(json[:5])
[{'codeCommune': '01001',
  'codePostal': '01400',
  'libelleAcheminement': "L'ABERGEMENT-CLEMENCIAT",
  'nomCommune': "L'Abergement-Clémenciat"},
 {'codeCommune': '01002',
  'codePostal': '01640',
  'libelleAcheminement': 'ABERGEMENT-DE-VAREY (L )',
  'nomCommune': "L'Abergement-de-Varey"},
 {'codeCommune': '01004',
  'codePostal': '01500',
  'libelleAcheminement': 'AMBERIEU-EN-BUGEY',
  'nomCommune': 'Ambérieu-en-Bugey'},
 {'codeCommune': '01005',
  'codePostal': '01330',
  'libelleAcheminement': 'AMBERIEUX-EN-DOMBES',
  'nomCommune': 'Ambérieux-en-Dombes'},
 {'codeCommune': '01006',
  'codePostal': '01300',
  'libelleAcheminement': 'AMBLEON',
  'nomCommune': 'Ambléon'}]

Quoi de plus simple.

Si une authentification HTTP basique est nécessaire, il est possible de passer le nom d'utilisateur et le mot de passe lors de l'exécution de la requête.

>>> req = requests.get('https://unpkg.com/codes-postaux@3.2.0/codes-postaux.json', auth=('username','password'))
>>> req.status_code
200
>>> req.request.headers
{'User-Agent': 'python-requests/2.22.0', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='}
>>> import base64
>>> base64.b64decode(req.request.headers.get('Authorization').split(' ')[1])
b'username:password'

En examinant l'en-tête de la requête, on s'aperçoit qu'une clé 'Authorization' a été ajouté, que le type est 'Basic' et le contenu, nom d'utilisateur et mot de passe, sont encodés en base64.

Si on décode le contenu à l'aide du module base64, on récupère les informations d'authentification.

D'autres méthodes d'authentification sont diponibles, tout est expliqué ici.

Pour finir, il est également possible d'utiliser une session pour effectuer plusieurs requêtes sur un site, ce qui permet de conserver les informations d'identification et les cookies pendant l'exécution de toutes les requêtes.

>>> sess = requests.Session()
>>> req = sess.get('https://unpkg.com/codes-postaux@3.2.0/codes-postaux.json')
>>> req.status_code
200

A vous de jouer.

Etiquettes: 

Python: Le module tempfile

Besoin d'un fichier temporaire pour y stocker provisoirement des données.

Nul besoin de se prendre la tête en créant un fichier de toute pièce où il faudra penser au répertoire de stockage, au nom du fichier, à sa suppression etc etc ...

Le module tempfile, disponible sous Python 2 et 3 permet de gérer des fichiers temporaires de leurs créations à leurs suppressions.

Fait partie des modules à utiliser sans modération.

Exemple:

>>> import tempfile
# Je créé un nouveau fichier temporaire
>>> mytmpfile = tempfile.NamedTemporaryFile()
# je peux afficher son nom
>>> mytmpfile.name
'/tmp/tmp00plrd7l'
# je peux y écrire du contenu très facilement
>>> mytmpfile.file.write(b'the first test')
>>> mytmpfile.file.write(b'the second test')
# tant que la méthode flush ou close (de la sous-classe file) n'a pas été exécutée, le contenu est stocké en mémoire
>>> with open(mytmpfile.name, 'rb') as f1:
...     f1.read()
...
...
b''
# J'enregistre le contenu dans le fichier
>>> mytmpfile.file.flush()
# je vérifie que le contenu a bien été enregistré (pour info)
>>> with open(mytmpfile.name, 'rb') as f1:
...     f1.read()
...
...
b'the first testthe second test'
# ATTENTION, la méthode flush (de la sous-classe file) permet d'enregistrer dans le fichier tout le contenu écrit avec la méthode write (entre deux flush)
# Il est donc possible de rajouter du contenu si nécessaire.
# La méthode close (de la sous-classe file) permet d'enregistrer dans le fichier tout le contenu écrit avec la méthode write et ferme le fichier.
# Il n'est donc plus possible de rajouter du contenu.
# Tant que le script est exécuté et que la méthode close de la classe tempfile n'est pas exécutée, le fichier temporaire est existant sur le disque.
# La commande suivante supprime définitivement le fichier sur le disque.
>>> mytmpfile.close()

Un tas d'options est disponible mais celles par défaut sont amplement suffisantes.

Python: Mettre à jour tous les paquets très simplement

Voici une astuce qui permet de maintenir à jour ses paquets Python sans trop se prendre la tête.

Premièrement, générer le fichier des paquets installés

# python -m pip freeze > requirement
# head requirement
aiofiles==0.6.0
certifi==2020.12.5
chardet==3.0.4
click==7.1.2
gunicorn==20.0.4
h11==0.11.0
httptools==0.1.1
idna==2.10
Jinja2==2.11.2
MarkupSafe==1.1.1

Deuxièmement, créer un fichier de mise à jour à partir du fichier requirement

# sed 's/==/>=/g' requirement > upgrade_requirement
# head upgrade_requirement
aiofiles>=0.6.0
certifi>=2020.12.5
chardet>=3.0.4
click>=7.1.2
gunicorn>=20.0.4
h11>=0.11.0
httptools>=0.1.1
idna>=2.10
Jinja2>=2.11.2
MarkupSafe>=1.1.1

Et enfin, lancer la mise à jour des paquets

# python -m pip install -U -r upgrade_requirement

Et voilà, c'est fini

 

Etiquettes: 

Python: Modifier la valeur d'une balise d'un XML en fonction de la valeur de son attribut

Voici un petit exemple qui permet de modifier la valeur d'une balise en fonction de son attribut et de sa valeur.

XML d'origine:

<xml>
    <ma_balise mon_attribut="ok">valeur1</ma_balise>
    <ma_balise mon_attribut="ko">valeur2</ma_balise>
    <ma_balise>valeur3</ma_balise>
    <ma_balise mon_attribut="ok">valeur4</ma_balise>
    <ma_balise>valeur5</ma_balise>
</xml>

Script Python:

>>> from lxml import etree
>>> xml = etree.parse('monfichier.xml')
>>> for ma_balise in xml.getchildren():
    if 'mon_attribut' in ma_balise.attrib and ma_balise.attrib['mon_attribut'] == 'ok':
        ma_balise.text = 'nouvelle valeur'
        # Si besoin d'une section CDATA
        # ma_balise.text = etree.CDATA('nouvelle valeur')
                
>>> with open('monfichier.xml', 'w') as f1:
    f1.write(etree.tounicode(xml))

>>>

XML après traitement:

<xml>
    <ma_balise mon_attribut="ok">nouvelle valeur</ma_balise>
    <ma_balise mon_attribut="ko">valeur2</ma_balise>
    <ma_balise>valeur3</ma_balise>
    <ma_balise mon_attribut="ok">nouvelle valeur</ma_balise>
    <ma_balise>valeur5</ma_balise>
</xml>

Seules les balises ayant l'attribut 'mon_attribut' et sa valeur égale à 'ok' ont été modifiées.

Python: Modifier le timezone d'un objet datetime

Un évènement en Martinique le 26 novembre 2020 à 12:42 (heure locale of course), ça fait quelle heure en Métropole ?

>>> from datetime import datetime
>>> from dateutil import tz

>>> MTQ = tz.gettz('America/Martinique')
>>> dt1 = datetime(2020, 11, 26, 12, 42, 0, tzinfo=MTQ)
>>> print(dt1)
2020-11-26 12:42:00-04:00
>>> repr(dt1)
"datetime.datetime(2020, 11, 26, 12, 42, tzinfo=tzfile('America/Martinique'))"
>>> FRA = tz.gettz('Europe/Paris')
>>> dt2 = dt1.astimezone(tz=FRA)
>>> print(dt2)
2020-11-26 17:42:00+01:00
>>> repr(dt2)
"datetime.datetime(2020, 11, 26, 17, 42, tzinfo=tzfile('Europe/Paris'))"

ça fait 17:42 heure de Paris (le même jour)

Un autre exemple:

>>> dt1 = datetime(2020, 11, 26, 20, 45, 0, tzinfo=MTQ)
>>> dt2 = dt1.astimezone(tz=FRA)
>>> print(dt1)
2020-11-26 20:45:00-04:00
>>> print(dt2)
2020-11-27 01:45:00+01:00

Le 26 novembre 2020 20:45 heure de Martinique, correspond au 27 novembre 2020 01:45 heure de Paris.

 

Python: MySQL

Des requêtes MySQL dans des scripts Python

 

Pré requis:

$ apt-get install python-mysqldb

Requête simple (select)

#!/usr/bin/env python
# -*- coding: utf-8 -*

# on importe le module MySQLdb
import MySQLdb

# On créé un dictionnaire contenant les paramètres de connexion MySQL
paramMysql = {
    'host'   : 'localhost',
    'user'   : 'username',
    'passwd' : 'password',
    'db'     : 'mabase'
}
# Bien respecter les noms des paramètres (host, user, passwd, db)

sql = """\
SELECT * FROM matable
WHERE monchamp1 = 'valeur1'
"""

try:
    # On  créé une conexion MySQL
    conn = MySQLdb.connect(**paramMysql)
    # On créé un curseur MySQL
    cur = conn.cursor(MySQLdb.cursors.DictCursor)
    # On exécute la requête SQL
    cur.execute(sql)
    # On récupère toutes les lignes du résultat de la requête
    rows = cur.fetchall()
    # On parcourt toutes les lignes
    for row in rows:
        # Pour récupérer les différentes valeurs des différents champs
        valeur1 = row['monchamp1']
        valeur2 = row['monchamp2']
        valeur3 = row['monchamp3']
        # etc etc ...

except MySQLdb.Error, e:
    # En cas d'anomalie
    print "Error %d: %s" % (e.args[0],e.args[1])
    sys.exit(1)

finally:
    # On ferme la connexion
    if conn:
        conn.close()

Requête insert, update, delete

#!/usr/bin/env python
# -*- coding: utf-8 -*

# on importe le module MySQLdb
import MySQLdb

# On créé un dictionnaire contenant les paramètres de connexion MySQL
paramMysql = {
    'host'   : 'localhost',
    'user'   : 'username',
    'passwd' : 'password',
    'db'     : 'mabase'
}
# Bien respecter les noms des paramètres (host, user, passwd, db)

sql = """\
INSERT INTO matable
(champ1, champ2, champ3)
VALUES ('valeur1', 'valeur2', 'valeur3')
"""

try:
    # On  créé une conexion MySQL
    conn = MySQLdb.connect(**paramMysql)
    # On créé un curseur MySQL
    cur = conn.cursor()
    try:
        # On exécute la requête SQL
        cur.execute(sql)
        # On commit
        conn.commit()
    except MySQLdb.Error, e:
        # En cas d'erreur on annule les modifications
        conn.rollback()

except MySQLdb.Error, e:
    # En cas d'anomalie
    print "Error %d: %s" % (e.args[0],e.args[1])
    sys.exit(1)

finally:
    # On ferme la connexion
    if conn:
        conn.close()

Python: PIP

Différentes manières d'installer pip pour Python 2 et 3:

Pré-requis:

$ apt-get install build-essential python-dev python3-dev

Pour python 2 (3 méthodes):

$ apt-get install python-pip

$ apt-get install python-setuptools
$ easy_install pip

$ curl https://bootstrap.pypa.io/get-pip.py | python

Pour python 3 (3 méthodes):

$ apt-get install python3-pip

$ apt-get install python3-setuptools
$ easy_install3 pip

$ curl https://bootstrap.pypa.io/get-pip.py | python3

Petite astuce pour ne plus être embêté avec pip quand on a plusieurs version de python installée.

Cette astuce fonctionne avec toutes les versions de python.

Remplacer le x par la version désirée

Lister tous les paquets obsolètes:

$ pythonx -m pip list -o

Installer un paquet:

$ pythonx -m pip install --upgrade mysqlclient

Installer une version spécifique d'un paquet:

$ pythonx -m pip install --upgrade mysqlclient==1.3.14

Python: Pandas DataFrame groupby resample

Comment analyser des données en les regroupant par les valeurs d'une colonne et en les ré-échantillonant, via une colonne "date", à chaque fin de mois.

J'ai un DataFrame Pandas "df1" avec les données suivantes:

>>> df1
      DATE        USER     DUREE
0     2020-09-03  USER#1   0.50
1     2020-09-02  USER#1   0.00
2     2020-09-02  USER#1   0.25
3     2020-09-01  USER#1   0.00
4     2020-09-01  USER#1   0.25
          ...     ...    ...
10715 2017-08-01  USER#2   0.75
10716 2017-07-19  USER#2   0.00
10717 2017-07-19  USER#2   0.00
10718 2017-07-17  USER#2   0.25
10719 2017-07-17  USER#2   0.00
[10720 rows x 3 columns]

J'ai donc 3 colonnes, une colonne "DATE", une colonne "USER" et une colonne "DUREE" avec les formats suivants:

>>> df1.dtypes
DATE     datetime64[ns]
USER             object
DUREE           float64
dtype: object

J'aimerais donc connaitre le cumul du temps passé par chaque "USER" à chaque fin de mois et obtenir le résultat suivant:

USER        USER#1  USER#2
DATE                     
2011-04-30   76.25    0.00
2011-05-31  109.75    0.00
2011-06-30   74.00    0.00
2011-07-31   31.25    0.00
2011-08-31   83.75    0.00
            ...     ...
2020-05-31    8.75   17.50
2020-06-30   25.50   18.25
2020-07-31    4.25   38.00
2020-08-31   92.75    5.25
2020-09-30    1.25    4.50
[114 rows x 2 columns]

Ce résultat peut-être obtenu en utilisant tout simplement 5 méthodes disponibles pour les DataFrame.

Premièrement, je ré-index mon DataFrame en utilisant la colonne "DATE" - Indispensable pour le ré-échantillonage

>>> df1.set_index('DATE')
              USER  DUREE
DATE                    
2020-09-03  USER#1   0.50
2020-09-02  USER#1   0.00
2020-09-02  USER#1   0.25
2020-09-01  USER#1   0.00
2020-09-01  USER#1   0.25
            ...    ...
2017-08-01  USER#2   0.75
2017-07-19  USER#2   0.00
2017-07-19  USER#2   0.00
2017-07-17  USER#2   0.25
2017-07-17  USER#2   0.00
[10720 rows x 2 columns]

Ensuite, j'effectue mon regroupement sur la colonne "USER"

>>> df1.set_index('DATE').groupby('USER')
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x00000151789EDEB0>

J'obtiens donc un objet "DataFrameGroupBy"

Pour le ré-échantillonage, j'utilise la méthode "resample" qui va agir sur les données contenues dans mon index (par défaut).
Le paramètre "M" va ré-échantilloner mes dates à chaque fin de mois.

>>> df1.set_index('DATE').groupby('USER').resample('M')
<pandas.core.resample.DatetimeIndexResamplerGroupby object at 0x000001517888F3A0>

J'obtiens donc un objet "DatetimeIndexResamplerGroupby"

Je fais maintenant la somme des valeurs de ma colonne "DUREE"

>>> df1.set_index('DATE').groupby('USER').resample('M')['DUREE'].sum()
USER    DATE     
USER#1  2011-04-30     76.25
        2011-05-31    109.75
        2011-06-30     74.00
        2011-07-31     31.25
        2011-08-31     83.75
                       ... 
USER#2  2020-05-31     17.50
        2020-06-30     18.25
        2020-07-31     38.00
        2020-08-31      5.25
        2020-09-30      4.50
Name: DUREE, Length: 153, dtype: float64

Pour finir, je souhaite transposer le premier niveau "USER" de mon index en colonne.
Pour cela j'utilise la méthode "unstack" avec le paramètre "level=0" pour lui indiquer le premier niveau de mon index et je lui demande de remplacer les valeurs nulles par "0" grâce à "fill_value=0"

>>> df1.set_index('DATE').groupby('USER').resample('M')['DUREE'].sum().unstack(level=0, fill_value=0)
USER        USER#1  USER#2
DATE                     
2011-04-30   76.25    0.00
2011-05-31  109.75    0.00
2011-06-30   74.00    0.00
2011-07-31   31.25    0.00
2011-08-31   83.75    0.00
            ...     ...
2020-05-31    8.75   17.50
2020-06-30   25.50   18.25
2020-07-31    4.25   38.00
2020-08-31   92.75    5.25
2020-09-30    1.25    4.50
[114 rows x 2 columns]

Et voilà, mes données ont été agrégées par "USER" et ré-échantillonées à chaque fin de mois.

Il est possible de filtrer les données par année

>>> df1.set_index('DATE').groupby('USER').resample('M')['DUREE'].sum().unstack(level=0, fill_value=0).loc['2020']
USER        USER#1  USER#2
DATE                     
2020-01-31   25.50   50.50
2020-02-29   35.25   15.25
2020-03-31   33.25   26.75
2020-04-30    6.50    1.25
2020-05-31    8.75   17.50
2020-06-30   25.50   18.25
2020-07-31    4.25   38.00
2020-08-31   92.75    5.25
2020-09-30    1.25    4.50

Ou de calculer le total par "USER"

>>> df1.set_index('DATE').groupby('USER').resample('M')['DUREE'].sum().unstack(level=0, fill_value=0).sum()
USER
USER#1    8219.5
USER#2    2432.5
dtype: float64

Ou de calculer le total par "USER" pour une année précise

>>> df1.set_index('DATE').groupby('USER').resample('M')['DUREE'].sum().unstack(level=0, fill_value=0).loc['2020'].sum()
USER
USER#1    233.00
USER#2    177.25
dtype: float64

Simple comme bonjour.

Merci Pandas

Python: Parser et indenter un flux XML

Parser un flux XML afin de le valider, extraire des données et l'afficher à l'écran avec une bonne indentation, tout ceci est possible grâce au module xml.dom.minidom.

Exemple avec Python3 et le flux XML suivant:

>>> flux = '<?xml version="1.0" encoding="UTF-8" standalone="no"?><geonames style="MEDIUM"><totalResultsCount>5</totalResultsCount><geoname><toponymName>Republic of Austria</toponymName><name>Austria</name><lat>47.33333</lat><lng>13.33333</lng><geonameId>2782113</geonameId><countryCode>AT</countryCode><countryName>Austria</countryName><fcl>A</fcl><fcode>PCLI</fcode></geoname><geoname><toponymName>Republic of France</toponymName><name>France</name><lat>46</lat><lng>2</lng><geonameId>3017382</geonameId><countryCode>FR</countryCode><countryName>France</countryName><fcl>A</fcl><fcode>PCLI</fcode></geoname><geoname><toponymName>Federal Republic of Germany</toponymName><name>Germany</name><lat>51.5</lat><lng>10.5</lng><geonameId>2921044</geonameId><countryCode>DE</countryCode><countryName>Germany</countryName><fcl>A</fcl><fcode>PCLI</fcode></geoname><geoname><toponymName>Repubblica Italiana</toponymName><name>Italy</name><lat>42.83333</lat><lng>12.83333</lng><geonameId>3175395</geonameId><countryCode>IT</countryCode><countryName>Italy</countryName><fcl>A</fcl><fcode>PCLI</fcode></geoname><geoname><toponymName>Principality of Liechtenstein</toponymName><name>Liechtenstein</name><lat>47.16667</lat><lng>9.53333</lng><geonameId>3042058</geonameId><countryCode>LI</countryCode><countryName>Liechtenstein</countryName><fcl>A</fcl><fcode>PCLI</fcode></geoname></geonames>'
>>> from xml.dom.minidom import parseString
>>> from pprint import pprint
>>> # La commande suivante permet de créer le parseur et de valider le flux XML par la même occasion
>>> parser = parseString(flux)
>>> # La commande suivante permet d'afficher le flux XML correctement indenté
>>> print(parser.toprettyxml())
<?xml version="1.0" ?>
<geonames style="MEDIUM">
    <totalResultsCount>5</totalResultsCount>
    <geoname>
        <toponymName>Republic of Austria</toponymName>
        <name>Austria</name>
        <lat>47.33333</lat>
        <lng>13.33333</lng>
        <geonameId>2782113</geonameId>
        <countryCode>AT</countryCode>
        <countryName>Austria</countryName>
        <fcl>A</fcl>
        <fcode>PCLI</fcode>
    </geoname>
    <geoname>
        <toponymName>Republic of France</toponymName>
        <name>France</name>
        <lat>46</lat>
        <lng>2</lng>
        <geonameId>3017382</geonameId>
        <countryCode>FR</countryCode>
        <countryName>France</countryName>
        <fcl>A</fcl>
        <fcode>PCLI</fcode>
    </geoname>
    <geoname>
        <toponymName>Federal Republic of Germany</toponymName>
        <name>Germany</name>
        <lat>51.5</lat>
        <lng>10.5</lng>
        <geonameId>2921044</geonameId>
        <countryCode>DE</countryCode>
        <countryName>Germany</countryName>
        <fcl>A</fcl>
        <fcode>PCLI</fcode>
    </geoname>
    <geoname>
        <toponymName>Repubblica Italiana</toponymName>
        <name>Italy</name>
        <lat>42.83333</lat>
        <lng>12.83333</lng>
        <geonameId>3175395</geonameId>
        <countryCode>IT</countryCode>
        <countryName>Italy</countryName>
        <fcl>A</fcl>
        <fcode>PCLI</fcode>
    </geoname>
    <geoname>
        <toponymName>Principality of Liechtenstein</toponymName>
        <name>Liechtenstein</name>
        <lat>47.16667</lat>
        <lng>9.53333</lng>
        <geonameId>3042058</geonameId>
        <countryCode>LI</countryCode>
        <countryName>Liechtenstein</countryName>
        <fcl>A</fcl>
        <fcode>PCLI</fcode>
    </geoname>
</geonames>

>>> # On peut également en profiter pour écrire le contenu dans un fichier et correctement indenté
>>> with open('monFlux.xml', 'w') as f:
    f.write(parser.toprettyxml())

    
1494
>>> # Afficher le nombre total d'éléments (correspond à la valeur de la balise totalResultsCount)
>>> print(parser.getElementsByTagName('totalResultsCount')[0].firstChild.data)
5
>>> # La commande suivante permet d'extraire les données d'un tag précis
>>> # et de les sauvegarder dans un dictionnaire
>>> datas = dict()
>>> geonames = parser.getElementsByTagName('geoname')
>>> for geoname in geonames:
    toponymName = geoname.getElementsByTagName('toponymName')[0].firstChild.data
    name = geoname.getElementsByTagName('name')[0].firstChild.data
    lat = geoname.getElementsByTagName('lat')[0].firstChild.data
    lng = geoname.getElementsByTagName('lng')[0].firstChild.data
    geonameid = geoname.getElementsByTagName('geonameId')[0].firstChild.data
    countrycode = geoname.getElementsByTagName('countryCode')[0].firstChild.data
    countryname = geoname.getElementsByTagName('countryName')[0].firstChild.data
    fcl = geoname.getElementsByTagName('fcl')[0].firstChild.data
    fcode = geoname.getElementsByTagName('fcode')[0].firstChild.data
    datas[geonameid] = {'toponymName': toponymName, 'name': name, 'lat': lat, 'lng': lng, 'countryCode': countrycode, 'countryName': countryname, 'fcl': fcl, 'fcode': fcode}

    
>>> pprint(datas)
{'2782113': {'countryCode': 'AT',
             'countryName': 'Austria',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.33333',
             'lng': '13.33333',
             'name': 'Austria',
             'toponymName': 'Republic of Austria'},
 '2921044': {'countryCode': 'DE',
             'countryName': 'Germany',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '51.5',
             'lng': '10.5',
             'name': 'Germany',
             'toponymName': 'Federal Republic of Germany'},
 '3017382': {'countryCode': 'FR',
             'countryName': 'France',
             
'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '46',
             'lng': '2',
             'name': 'France',
             'toponymName': 'Republic of France'},
 '3042058': {'countryCode': 'LI',
             'countryName': 'Liechtenstein',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '47.16667',
             'lng': '9.53333',
             'name': 'Liechtenstein',
             'toponymName': 'Principality of Liechtenstein'},
 '3175395': {'countryCode': 'IT',
             'countryName': 'Italy',
             'fcl': 'A',
             'fcode': 'PCLI',
             'lat': '42.83333',
             'lng': '12.83333',
             'name': 'Italy',
             'toponymName': 'Repubblica Italiana'}}
>>>

C'est assez simple dans l'ensemble.

Python: Rechercher tous les anagrammes dans un texte

Selon wikipédia, une anagramme est un mot ou une expression obtenu en permutant les lettres d'un mot ou d'une expression de départ.

Pour l'exemple, je vais rechercher tous les anagrammes présents dans l'oeuvre de Jules Verne, "Le Tour Du Monde En 80 Jours".

Première étape, récupérer l'oeuvre au format txt.
Le site gutenberg permet de faire ce genre de choses.

Le module requests permet d'exécuter des requêtes HTTP

$ python3 -m pip install --upgrade requests
>>> import requests
>>> r1 = requests.get('https://www.gutenberg.org/files/800/800-8.txt')

Récupération du texte dans la réponse de la requête et nettoyage de tous les caractères non indispensables.
Le texte commence au 3 941 ème caractères et se termine au 431 252 ème caractères.
Tous les accents sont supprimés.
Tout le texte est converti en minuscule.
La regex recherche tous les mots de 3 caractères et plus.
Tous les mots en double sont supprimés.

Pour la suppression des caractères accentués, j'utilise le module removeaccents

$ python3 -m pip install --upgrade removeaccents
>>> from removeaccents.removeaccents import remove_accents
>>> text = r1.text[3941:431252]
>>> text = remove_accents(text)
>>> text = text.lower()
>>> words = re.findall(r'\b[A-Za-z]{3,}\b', text)
>>> words = list(set(words))

Nous nous retrouvons avec une liste de 8426 mots de 3 caractères et plus.

>>> len(words)
8426

Nous allons maintenant indexer tous les anagrammes dans un dictionnaire.

>>> anagrammes = dict()
>>> for word in words:
    letters = ''.join(sorted(word))
    if letters in anagrammes:
        anagrammes.get(letters).append(word)
    else:
        anagrammes[letters] = [word]
>>> anagrammes = {k: v for k, v in anagrammes.items() if len(v) > 1}

Nous obtenons une liste de 312 anagrammes

>>> len(anagrammes)
312

Voici la liste des 30 premiers anagrammes triée par ordre décroissant du nombre de mots.

>>> print(sorted(anagrammes.items(), key=lambda x: len(x[1]), reverse=True)[:30])
[('aerstu', ['sauter', 'autres', 'surate']), ('eeprst', ['pertes', 'pester', 'pretes']), ('eeelrv', ['releve', 'revele', 'elever']), ('aeenprst', ['esperant', 'presenta', 'separent']), ('aeiprt', ['partie', 'pirate', 'patrie']), ('efmnort', ['froment', 'forment', 'fremont']), ('aisv', ['vais', 'avis', 'visa']), ('aceinrt', ['crainte', 'ecriant', 'certain']), ('aceerss', ['sacrees', 'caresse', 'cessera']), ('aerrs', ['rares', 'raser', 'serra']), ('aeirstt', ['artiste, 'restait', 'attires']), ('eimprs', ['permis', 'mepris', 'primes']), ('eegnr', ['green', 'genre', 'gener']), ('adegr', ['garde', 'egard', 'grade']), ('eirsv', ['servi', 'viser', 'rives']), ('aeegilns', ['inegales', 'alignees', 'signalee']), ('aegnt', ['agent', 'geant', 'etang']), ('acert', ['ecart', 'trace', 'carte']), ('eeinrtt', ['interet', 'retenti', 'retient']), ('adegnrs', ['grandes', 'dangers', 'gardens']), ('eerrsv', ['verres', 'revers', 'verser']), ('aeirrtt', ['traiter', 'attirer', 'traitre']), ('acers', ['sacre', 'races', 'arecs']), ('ersu', ['user', 'rues', 'ruse']), ('cdeirt', ['decrit', 'credit', 'direct']), ('einru', ['nuire', 'ruine', 'reuni']), ('bceimno', ['combien', 'combine']), ('eirtv', ['vitre', 'revit']), ('aceimnoprt', ['importance', 'comprenait']), ('aceinrst', ['certains', 'craintes'])]

 

Python: Rediriger la sortie standard vers un fichier

En Python, comme en Bash, il est possible de rediriger tous les flux des commandes d'affichages (print, pprint, etc...) vers un fichier.

Voici un exemple tout simple:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
from pprint import pprint
# We open the log file in writting mode
with open('myLogFile', 'w') as f:
    # We redirect the 'sys.stdout' command towards the descriptor file
    sys.stdout = f
    # Now, all the print commands write directly in the file
    print('This is a test.')
    D = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
    pprint(D)
    A = 'Hello, how are you ?\n'
    sys.stdout.write(A)

Quand on exécute le script:

# python3 testStdoutToFile.py
# cat myLogFile
This is a test.
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
Hello, how are you ?

Soo simple !

Python: Requests: appeler un webservice SOAP

Pour l'exemple je vais appeler un webservice SOAP qui me retourne le résultat d'une addition de deux entiers.

J'ai utilisé SoapUI afin de récupérer le format du fichier XML à utiliser.
Le WSDL du webservice SOAP est disponible à cette adresse http://www.dneonline.com/calculator.asmx?wsdl

Le WSDL contient toutes les informations nécessaires pour l'utilisation du webservice.

>>> import requests
>>> url = 'http://www.dneonline.com/calculator.asmx'
>>> xml = '''\
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:tem="http://tempuri.org/">
   <soap:Header/>
   <soap:Body>
      <tem:Add>
         <tem:intA>50</tem:intA>
         <tem:intB>20</tem:intB>
      </tem:Add>
   </soap:Body>
</soap:Envelope>'''
>>> headers = {'content-type': 'application/soap+xml; charset=utf-8'}
>>> r1 = requests.post(url, data=xml, headers=headers)
>>> print(r1.text)
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <soap:Body>
        <AddResponse xmlns="http://tempuri.org/">
            <AddResult>70</AddResult>
        </AddResponse>
    </soap:Body>
</soap:Envelope>

Pour un appel de webservice SOAP, il est très important de modifier le content-type du header de la requête.

Parfois, il faut indiquer 'application/soap+xml; charset=utf-8' et d'autres fois 'text/xml; charset=utf-8'.

Ca dépend du webservice SOAP.
Si le content-type n'est pas correct, l'erreur sera renseignée dans la réponse.

Dans mon exemple, le résultat de l'addition est retourné dans la balise <AddResult>

Python: Rotation des logs Apache

Voici un petit script en Python qui permet de faire la rotation des fichiers de log d'un serveur Apache.

# -*- coding: UTF-8 -*-

"""
    logrotate.py
    ============
    
    Permet la rotation des logs Apache
    
    :Example:
    
    >>> import logrotate
    >>> logrotate.main()
"""

import os, shutil, zipfile
import datetime as dt

F = r'C:\wamp\logs'                                 # Répertoire des logs Apache
LOG = 'logrotate.log'                               # Fichier de log pour les rotations
TXT = 'ROTATION DU FICHIER'                         # Texte affiché dans le log des rotations
TXT2 = 'SUPPRESSION DU FICHIER'                     # Texte affiché dans le log des rotations
TXT3 = 'COMPRESSION DU FICHIER'                     # Texte affiché dans le log des rotations
NBARCHIVE = 20                                      # Nombre de fichier de log historisé .0 .1 .2 etc etc ...

def now():
    """
        Retourne la date et l'heure courante au format ISO 2018-06-28T10:30:23.816122
        
        :return: La date et heure courante
        :rtype:  datetime.datetime
    """
    return dt.datetime.now().isoformat()
    
def cleanList(LIST = [], LISTERR = []):
    """
        Supprime de la liste ``LIST`` tous les fichiers 
        correspondant à ceux en erreur présent dans la liste ``LISTERR``
        
        :return: Une nouvelle liste de fichiers nettoyées
        :rtype:  list
    """
    for ERR in LISTERR:
        LIST = list(filter(lambda f: not f.startswith(ERR), LIST))
    return LIST

def log(FLOG, TXT, FILE, MSG):
    """
        Ecrit dans le fichier ``FLOG`` les infos
        ``TXT``, ``FILE`` et ``MSG`` avec la date courante
    """
    print('{DATETIME} {T:<25s} {FILE:<40s} --> {MSG}'.format(DATETIME=now(), FILE=FILE, T=TXT, MSG=MSG), file=FLOG)

def delFile(FILE, FLOG):
    """
        Supprime le fichier ``FILE``
    """
    try: os.remove(FILE)
    except: log(FLOG, TXT2, FILE, 'KO')
    else: log(FLOG, TXT2, FILE, 'OK')

def listLog(LIST, EXT):
    """
        Extrait tous les fichiers de log avec l'extension ``EXT``
        de la liste ``LIST``

        :return: Une liste contenant uniquement les fichiers ayant l'extension ``EXT``
        :rtype:  list
    """
    return list(filter(lambda f: f.endswith(EXT), LIST))

def removeUnusedFiles(FLOG):
    """
        Supprime tous les fichiers qui ne sont plus concernés par des fichiers LOG
        Retourne une nouvelle liste avec les fichiers supprimés en moins

        :return: Une nouvelle liste avec les fichiers supprimés en moins
        :rtype:  list
    """
    LIST = os.listdir(F)
    LISTLOG = listLog(LIST, '.log')
    LISTDEL = list(filter(lambda x: '.'.join(x.split('.')[:2]) not in LISTLOG, LIST))
    for FILE in LISTDEL:
        delFile(os.path.join(F, FILE), FLOG)
    return list(set(LIST) - set(LISTDEL))

def incrementZipFile(LIST, FLOG, LISTERR = []):
    """
        Incrémente les archives ZIP jusqu'à ``NBARCHIVE``
        
        :return: Une liste de fichiers en erreur
        :rtype:  list
    """
    LISTLOG = listLog(LIST, '.log')
    LISTZIP = listLog(LIST, '.zip')
    for f in LISTLOG:
        WLIST = sorted(list(filter(lambda x: x.startswith(f), LISTZIP)), key=lambda y: int(y.split('.')[2]), reverse=True)
        ERR = False
        for FILE in WLIST:
            TF = FILE.split('.')
            IDX = int(TF[2])
            if IDX < NBARCHIVE:
                OLDFILE = os.path.join(F, FILE)
                TF[2] = str(IDX + 1)
                NEWFILE = os.path.join(F, '.'.join(TF))
                try:
                    with zipfile.ZipFile(OLDFILE, mode='r', compression=zipfile.ZIP_DEFLATED) as f1:
                        try:
                            CT = f1.read(os.path.splitext(FILE)[0])
                        except: ERR = True
                        else:
                            BASE = os.path.splitext(os.path.basename(NEWFILE))[0]
                            try:
                                with zipfile.ZipFile(NEWFILE, mode='w', compression=zipfile.ZIP_DEFLATED) as f2:
                                    f2.writestr(BASE, CT)
                            except: ERR = True
                            else: log(FLOG, TXT, OLDFILE, '{NEWFILE}'.format(NEWFILE=NEWFILE))
                except: ERR = True
                finally:
                    if ERR:
                        LISTERR.append(f)
                        log(FLOG, TXT, OLDFILE, '{NEWFILE} ***KO***'.format(NEWFILE=NEWFILE))
                        break
    return LISTERR

def log_0ToZip(LIST, FLOG, LISTERR = []):
    """
        On écrit tout le contenu des fichiers LOG *.log.0
        dans de nouveaux fichiers ZIP *.log.1.zip
        On supprime ensuite tous les fichiers *.log.0
        
        :return: Une liste de fichiers en erreur
        :rtype:  list
    """
    LISTLOG0 = listLog(LIST, '.log.0')
    for FILE in LISTLOG0:
        ERR = False
        with open(os.path.join(F, FILE), mode='r', encoding='UTF-8') as f1:
            BASE = FILE[:-1] + '1'
            ZIP = os.path.join(F, BASE + '.zip')
            with zipfile.ZipFile(ZIP, 'w', compression=zipfile.ZIP_DEFLATED) as f2:
                try: f2.writestr(BASE, f1.read().encode('UTF-8'))
                except: 
                    ERR = True
                    LISTERR.append(FILE[:-2])
                    log(FLOG, TXT3, os.path.join(F, FILE), '{NEWFILE} ***KO***'.format(NEWFILE=ZIP))
                else: log(FLOG, TXT3, os.path.join(F, FILE), '{NEWFILE}'.format(NEWFILE=ZIP))
        if not ERR: delFile(os.path.join(F, FILE), FLOG)
    return LISTERR

def logTo0(LIST, FLOG):
    """
        On écrit tout le contenu des fichiers LOG *.log 
        dans de nouveaux fichiers numérotés *.log.0
        On écrase ensuite le contenu des fichiers LOG
    """
    LISTLOG = listLog(LIST, '.log')
    for FILE in LISTLOG:
        OLDFILE = os.path.join(F, FILE)
        NEWFILE = os.path.join(F, FILE + '.0')
        try:
            with open(OLDFILE, mode='r', encoding='UTF-8') as f1:
                with open(NEWFILE, mode='w', encoding='UTF-8') as f2:
                    f2.write(f1.read())
        except: log(FLOG, TXT, OLDFILE, '{NEWFILE} ***KO***'.format(NEWFILE=NEWFILE))
        else: 
            log(FLOG, TXT, OLDFILE, '{NEWFILE}'.format(NEWFILE=NEWFILE))
            try:
                with open(OLDFILE, mode='w', encoding='UTF-8') as f1:
                    f1.write('')
            except: log(FLOG, TXT2, OLDFILE, 'KO')
            else: log(FLOG, TXT2, OLDFILE, 'OK')

def main():
    """
        Fonction principale
        
        Pour chaque fichier de log (*.log) trouvé dans le dossier (variable ``F``)
        Chaque niveau d'archive est incrémenté de 1 dans la limite du nombre d'archive
        indiqué dans la variable ``NBARCHIVE``
    """
    with open(os.path.join(F, LOG), mode='a', encoding='UTF-8') as FLOG:
        LIST = removeUnusedFiles(FLOG)
        LIST.remove(LOG) # On supprime de la liste le fichier de LOG du script
        LISTERR = incrementZipFile(LIST, FLOG)
        LIST = cleanList(LIST, LISTERR)
        LISTERR = log_0ToZip(LIST, FLOG, LISTERR)
        LIST = cleanList(LIST, LISTERR)
        logTo0(LIST, FLOG)

if __name__ == '__main__':
    main()

Et pour l'exécuter (après avoir adapté les variables F, LOG, NBARCHIVE):

$ python3 logrotate.py

Fonctionne sous Linux et Windows

Une tâche cron sous Linux ou une tâche planifiée sous Windows permet d'automatiser l'exécution du script toutes les semaines par exemple.

Le script peut-être téléchargé via le lien ci-dessous

https://git.quennec.fr/ronan/scripts_pub/raw/master/logrotate.py

Python: SQLite: select avec le nom des colonnes

Par défaut, lors d'une requête select sur une table SQLite, les valeurs des colonnes sont indexées par numéro, la position de la colonne dans le résultat.

>>> import sqlite3
>>> from faker import Faker
>>> fake = Faker('fr_FR')
>>> con = sqlite3.connect(':memory:')
>>> cur = con.cursor()
>>> cur.execute('create table contact (id integer primary key, firstname text, lastname text, email text, mobile_phone text)')
>>> for x in range(100):
    cur.execute('insert into contact (firstname, lastname, email, mobile_phone) values (?, ?, ?, ?)', (fake.first_name(), fake.last_name(), fake.email(), fake.phone_number()))
>>> for row in cur.execute('select * from contact where firstname = ?', ('Pierre', )):
    print(row)

(38, 'Pierre', 'Collin', 'marc71@bigot.com', '+33 2 53 97 25 45')

>>> cur.execute('select * from contact where firstname = ?', ('Pierre', ))
>>> cur.fetchall()
[(38, 'Pierre', 'Collin', 'marc71@bigot.com', '+33 2 53 97 25 45')]

L'objet row retourné correspond à un tuple, par conséquent pour obtenir, par exemple, la valeur de l'adresse mail, il faut utiliser l'index correspondant, c'est à dire 3.

>>> for row in cur.execute('select * from contact where firstname = ?', ('Pierre', )):
    print(row[3])

marc71@bigot.com

Pour pouvoir utiliser le nom des colonnes, il faut obligatoirement paramétrer la connexion de cette manière:

>>> con.row_factory = sqlite3.Row
>>> cur = con.cursor()
>>> for row in cur.execute('select * from contact where firstname = ?', ('Pierre', )):
    drow = dict(row) # on convertit l'objet row en objet dict
    print(drow)

{'id': 38, 'firstname': 'Pierre', 'lastname': 'Collin', 'email': 'marc71@bigot.com', 'mobile_phone': '+33 2 53 97 25 45'}

>>> for row in cur.execute('select * from contact where firstname = ?', ('Pierre', )):
    # on peut accéder au noms de colonnes directement
    print(row['email'])

marc71@bigot.com

>>> cur.execute('select * from contact where firstname = ?', ('Pierre', ))
>>> list(map(dict, cur.fetchall()))
[{'id': 38, 'firstname': 'Pierre', 'lastname': 'Collin', 'email': 'marc71@bigot.com', 'mobile_phone': '+33 2 53 97 25 45'}]

 

Python: SQlite3 - Utilisée une base de donnée en mémoire

Avec Python, il est possible d'utiliser une base de donnée SQlite montée en mémoire vive.

Très pratique dans le cas d'une utilisation occasionnelle et non persistente (analyse d'un fichier csv par exemple).

Exemple avec Python3

Pour l'exemple, j'utilise le fichier des codes postaux français.

#!/usr/bin/env python3

import csv
import sqlite3
import time

# Execution start time
start = time.time()

# Fields list
fieldsList = [
    'countryCode', 'postalCode', 'placeName', 'adminName1', 'adminCode1', 'adminName2',
    'adminCode2', 'adminName3', 'adminCode3', 'latitude', 'longitude', 'accuracy'
]

# Create database in memory
con = sqlite3.connect(':memory:')
cur = con.cursor()

# Create table in database
cur.execute('create table postalCodes \
    (id integer primary key, {0} text, {1} text, {2} text, \
    {3} text, {4} text, {5} text, {6} text, {7} text, \
    {8} text, {9} real, {10} real, {11} integer)'.format(*fieldsList)
)

# Insert request
sqlInsert = 'insert into postalCodes ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}) \
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'.format(*fieldsList)

# Read file and insert values into table 'postalCodes'
with open('FR.txt', 'r', newline='', encoding='utf-8') as f:
    reader = csv.reader(f, delimiter='\t')
    for row in reader:
        cur.execute(sqlInsert, row)
        
# Commit
con.commit()

# Execution end time
end = time.time()

# Start of analysis
for row in cur.execute('select count(*) from postalCodes'):
    totalLines = row[0]
    
print("{:n} lines inserted in {:f} sec.".format(totalLines, end - start), end='\n\n')

# Result
51141 lines inserted in 0.493353 sec.

print("All lines with the postal code: '44150'.")
for row in cur.execute('select * from postalCodes where postalCode = ?', ('44150', )):
    print(row)

# Result
All lines with the postal code: '44150'.
(27254, 'FR', '44150', 'Saint-Herblon', 'Pays de la Loire', '52', 'Loire-Atlantique', '44', 'Arrondissement d’Ancenis', '444', 47.4079, -1.0974, 5)
(27255, 'FR', '44150', 'Anetz', 'Pays de la Loire', '52', 'Loire-Atlantique', '44', 'Arrondissement d’Ancenis', '444', 47.3806, -1.1058, 5)
(27256, 'FR', '44150', 'Saint-Géréon', 'Pays de la Loire', '52', 'Loire-Atlantique', '44', 'Arrondissement d’Ancenis', '444', 47.3677, -1.2026, 5)
(27257, 'FR', '44150', 'Ancenis', 'Pays de la Loire', '52', 'Loire-Atlantique', '44', 'Arrondissement d’Ancenis', '444', 47.3667, -1.1667, 5)
    
print()
print("Minimum and maximum postal code for the adminCode2: 'Côtes-d'Armor'.")
for row in cur.execute('select min(postalCode), max(postalCode) from postalCodes where adminName2 = ?', ('Côtes-d\'Armor', )):
    print(row)

# Result
Minimum and maximum postal code for the adminCode2: 'Côtes-d'Armor'.
('22000', '22980')

print()
print("The number of placeName by adminName1 and adminName2.")    
for row in cur.execute('select adminName1, adminName2, count(placeName) from postalCodes group by adminName1, adminName2'):
    print(row)

# Result
The number of placeName by adminName1 and adminName2.
('', '', 1)
('Alsace-Champagne-Ardenne-Lorraine', 'Ardennes', 496)
('Alsace-Champagne-Ardenne-Lorraine', 'Aube', 497)
('Alsace-Champagne-Ardenne-Lorraine', 'Bas-Rhin', 802)
...
('Île-de-France', 'Yvelines', 601)

Super pratique, non !!!

Python: Supprimer tous les dossiers vides sur un serveur FTP

Voici un script Python qui permet, comme le titre l'indique, de parcourir toute l'arborescence d'un serveur FTP à la recherche des dossiers vides et de les supprimer.

Ce script a été testé avec Python 2.7, 3.4 et 3.5.

Détail du script:

La fonction principale qui permet de parcourir, récursivement, l'arborescence du serveur FTP.

def checkFtpEmptyFolders(ftp, dir):
    ctn = 0
    l = []
    ftp.dir(dir, l.append)
    l = [x.split() for x in l]
    """
        If the list is empty, the folder is empty.
        So, deletion of the folder.
    """
    if len(l) == 0:
        try:
            print("Delete folder %s" % dir)
            ftp.rmd(dir)
            ctn = 1
        except:
            pass
    
    else:
        """
            Browse the folder.
            If the item is a folder,
            then, recall the function
            with the subfolder in parameter.
        """
        for x in l:
            try:
                _dir = (dir + "/" + x[-1]).replace('//', '/')
                ftp.cwd(_dir)
                ftp.cwd(dir)
                ctn = ctn | checkFtpEmptyFolders(ftp, _dir)
            except:
                continue
    return ctn

Et enfin, la méthode pour appeler cette fonction.

from ftplib import FTP

host   = 'monFtp.monDomaine.fr'
user   = 'monUser'
passwd = 'monPass'
# Indiquer "/" pour débuter à la racine
dir    = '/monDossier'
# Cette variable permet de sortir de la boucle while
# s'il n'y a plus de dossier vide à supprimer
ctn    = 1

try:
    ftp = FTP(host)
    ftp.login(user, passwd)
    ftp.set_pasv(1)
    ftp.cwd(dir)
    while ctn:
        ctn = checkFtpEmptyFolders(ftp, dir)
    ftp.quit()
finally:
    try:
        ftp.close()
    except:
        pass

Et voilà, tous les dossiers vides, enfin supprimés.

Plusieurs adaptations peuvent être faites à partir de ce script.

Etiquettes: 

Python: Trier le contenu d'un dictionnaire (dict)

En python, l'objet dict ne conserve pas l'ordre dans lequel les éléments sont ajoutés et ne possède pas de fonction sort permettant de trier les données suivants les clés ou les valeurs.

Pour trier un objet dict, il suffit d'utiliser la fonction sorted.
Cette fonction retourne une liste contenant les valeurs triées.
Dans le cas d'un objet dict, les données (clés + valeurs) sont converties en tuple.

Exemple:

>>> d1 = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'a1': 5, 'b1': 10, 'c1': 8, 'd1': 6}
>>> print(d1)
{'d1': 6, 'c': 3, 'd': 4, 'b': 2, 'a1': 5, 'c1': 8, 'b1': 10, 'a': 1}

On voit que l'ordre d'insertion n'a pas été conservé.

Tri de l'objet dict en fonction des clés

>>> sorted(d1.items(), key=lambda t: t[0])
[('a', 1), ('a1', 5), ('b', 2), ('b1', 10), ('c', 3), ('c1', 8), ('d', 4), ('d1', 6)]

On utilise donc la fonction sorted avec en paramètre la liste des données (clé + valeurs) de l'objet dict (la fonction items() de l'objet dict) et en clé de tri, une fonction lambda indiquant l'indice à utiliser pour le tri.

L'indice '0' (zéro) indique la clé de l'objet dict.
Pour un tri suivant les valeurs, il suffit d'indiquer l'indice '1' (un)

>>> sorted(d1.items(), key=lambda t: t[1])
[('a', 1), ('b', 2), ('c', 3), ('d', 4), ('a1', 5), ('d1', 6), ('c1', 8), ('b1', 10)]

Si les valeurs sont des tuples ou des lists, il suffit d'indiquer, un second indice (x), correspondant à la position sur laquelle on souhaite faire le tri.

>>> sorted(d1.items(), key=lambda t: t[1][x])

Pour conserver l'ordre de tri dans un objet dict, il est obligatoire d'utiliser l'objet OrderedDict du module collections.

>>> from collections import OrderedDict
>>> d2 = OrderedDict(sorted(d1.items(), key=lambda t: t[0]))
>>> print(d2)
OrderedDict([('a', 1), ('a1', 5), ('b', 2), ('b1', 10), ('c', 3), ('c1', 8), ('d', 4), ('d1', 6)])

L'itération d'un objet OrderedDict s'utilise de la même manière que pour un objet dict.

>>> for k, v in d2.items():
    print("{:>3s} : {:<d}".format(k, v))

  a : 1
 a1 : 5
  b : 2
 b1 : 10
  c : 3
 c1 : 8
  d : 4
 d1 : 6

infoTesté avec Python2 et Python3

Python: Trier les balises d'un fichier XML avec BeautifulSoup

BeautifulSoup est un module Python qui permet de manipuler très facilement n'importe quel fichier XML.

Pour l'installer, rien de plus simple que:

$ python3 -m pip install --upgrade bs4

Exemple avec le fichier XML suivant

<?xml version="1.0" encoding="utf-8"?>
<towns>
 <town name="Paris"/>
 <town name="Lyon"/>
 <town name="Marseille"/>
 <town name="Nantes"/>
 <town name="Ancenis"/>
 <town name="Bordeaux"/>
 <town name="Toulouse"/>
 <town name="Rouen"/>
 <town name="Brest"/>
</towns>

On importe le XML à l'aide du module BeautifulSoup

>>> from bs4 import BeautifulSoup as bs
>>> xml = bs("""<?xml version="1.0" encoding="utf-8"?>
<towns>
 <town name="Paris"/>
 <town name="Lyon"/>
 <town name="Marseille"/>
 <town name="Nantes"/>
 <town name="Ancenis"/>
 <town name="Bordeaux"/>
 <town name="Toulouse"/>
 <town name="Rouen"/>
 <town name="Brest"/>
</towns>""", 'xml')
>>> xml
<?xml version="1.0" encoding="utf-8"?>
<towns>
<town name="Paris"/>
<town name="Lyon"/>
<town name="Marseille"/>
<town name="Nantes"/>
<town name="Ancenis"/>
<town name="Bordeaux"/>
<town name="Toulouse"/>
<town name="Rouen"/>
<town name="Brest"/>
</towns>
>>>

Je souhaite trier par ordre croissant toutes les balises town en fonction de la valeur de l'attribut name.

Pour cela, je vais dans un premier temps récupérer toutes les balises town.

>>> towns = xml.findAll('town')
>>> towns
[<town name="Paris"/>, <town name="Lyon"/>, <town name="Marseille"/>, <town name="Nantes"/>, <town name="Ancenis"/>, <town name="Bordeaux"/>, <town name="Toulouse"/>, <town name="Rouen"/>, <town name="Brest"/>]

Me voici donc avec une liste towns contenant toutes mes balises town.

Ensuite, je trie par ordre croissant le contenu de ma liste towns.

>>> towns.sort(key=lambda x: x.get('name'))
>>> towns
[<town name="Ancenis"/>, <town name="Bordeaux"/>, <town name="Brest"/>, <town name="Lyon"/>, <town name="Marseille"/>, <town name="Nantes"/>, <town name="Paris"/>, <town name="Rouen"/>, <town name="Toulouse"/>]

Pour trier correctement le contenu de ma liste, j'utilise le paramètre key de la fonction sort en y indiquant une fonction lambda dans laquelle je lui indique d'utiliser l'attribut name pour effectuer le tri.

Avec BeautifulSoup, pour obtenir la valeur d'un attribut d'une balise, il suffit d'utiliser la méthode get sur n'importe quel élément avec le nom de l'attribut, en l'occurence name.

Dans ma fonction lambda, x étant remplacé par chaque élément town de la liste.

Me voici donc avec ma liste parfaitement trié.

Il suffit donc maintenant de remplacer tout le contenu de la balise towns de mon XML avec le contenu de ma liste.

>>> xml.towns.clear()
>>> xml.towns
<towns/>
>>> xml.towns.extend(towns)
>>> print(xml.prettify())
<?xml version="1.0" encoding="utf-8"?>
<towns>
 <town name="Ancenis"/>
 <town name="Bordeaux"/>
 <town name="Brest"/>
 <town name="Lyon"/>
 <town name="Marseille"/>
 <town name="Nantes"/>
 <town name="Paris"/>
 <town name="Rouen"/>
 <town name="Toulouse"/>
</towns>

J'ai donc utilisé la méthode clear de l'élément towns pour vider tous les éléments town.

Enfin, comme pour une liste Python, la méthode extend de l'élément towns permet d'ajouter tout le contenu de la liste towns correctement triée.

Simple comme bonjour, isn't it....

Etiquettes: 

Python: Utiliser un fichier de paramètres

Sous Python, il est possible d'utiliser dans les scripts des fichiers de paramètres à la manière des fichier INI sous Windows.

Un fichier de paramètres doit contenir une ou plusieurs sections et pour chaque section, une paire paramètre/valeur.

Exemple:

$ cat monFichierDeParametres
[MYSQL]
host: localhost
user: toto
pass: pass4toto
db: maBase

[MAIL]
server: monserveursmtp.fr
from: toto@domaine.com
to: tutu@domaine.com
sujet: bla bla bla
  • Une section doit être écrite entre crochets [...]
  • Le caractère ":" doit être utilisé pour séparer le paramètre de la valeur


Utilisation dans un script Python:

#!/usr/bin/env python
# -*- coding: utf-8 -*
import ConfigParser # Permet de parser le fichier de paramètres
config = ConfigParser.RawConfigParser() # On créé un nouvel objet "config"
config.read('monFichierDeParametres') # On lit le fichier de paramètres
# On récupère les valeurs des différents paramètres
# ATTENTION, cette syntaxe est spécifique pour les paramètres MySQL
# On créé un dictionnaire contenant les paires clés/valeurs
# Pour chaque paramètre, on utilise la fonction "get" de notre objet "config" en lui indiquant la section et le nom du paramètre
paramMysql = {
    'host'   : config.get('MYSQL','host'),
    'user'   : config.get('MYSQL','user'),
    'passwd' : config.get('MYSQL','pass'),
    'db'     : config.get('MYSQL','db')
}
# Récupération basique dans des variables
serveurMail = config.get('MAIL','server')
from = config.get('MAIL','from')
to = config.get('MAIL','to')
sujet = config.get('MAIL','sujet')

C'est quand même vachement pratique.

Etiquettes: 

Python: Vérifier la version TLS d'une serveur

Pour vérifier la version TLS d'un serveur Python propose 2 modules qui permet de le faire.

Voici un exemple qui permet de controler la version TLS de mon serveur.

import socket
import ssl

hostname = 'quennec.fr'
context = ssl.create_default_context()

with socket.create_connection((hostname, 443)) as sock:
    with context.wrap_socket(sock, server_hostname=hostname) as ssock:
        print(ssock.version())
        
TLSv1.2

La version TLS de mon serveur est la 1.2

Etiquettes: 

Python: décortiquer une URL à l'aide d'une regex

Cette regex permet de décortiquer une URL et de nommer les différents éléments.

regexp_url = "^(?i)\
(?P<proto>(http(s)*|ftp|ssh))\
(://)\
((?P<user>\w+)(:(?P<password>\w+))?@)?\
(?P<hostname>[\w\.-]+)\
(:(?P<port>[0-9]+))?\
(?P<path>.*)?\
$"

Je l'ai mise sur plusieurs lignes pour mieux la comprendre (d'où les '\' à chaque fin de ligne).

Sur la première ligne, le groupement (?i) permet d'indiquer que la regex doit être insensible à la casse.

La seconde ligne permet de parser et mémoriser le protocole utilisé (http/https/ftp/ssh).

La quatrième ligne permet de parser et mémoriser, si besoin, le user et le password.

La cinquième ligne permet de parser et mémoriser le hostname.

La sixième ligne permet de parser et mémoriser, si besoin, le port.

Pour terminer, la septième ligne permet de parser et mémoriser, si besoin, le path.

Le couple user:password est facultatif ainsi que le port et le path.

Quelques exemples

>>> import re
>>> from pprint import pprint
>>> regexp_url = "^(?i)\
(?P<proto>(http(s)*|ftp|ssh))\
(://)\
((?P<user>\w+)(:(?P<password>\w+))?@)?\
(?P<hostname>[\w\.-]+)\
(:(?P<port>[0-9]+))?\
/\
(?P<path>.*)?\
$"
>>> pprint(re.match(regexp_url, 'https://www.google.fr/').groupdict())
{'hostname': 'www.google.fr',
 'password': None,
 'path': '/',
 'port': None,
 'proto': 'https',
 'user': None}
>>> pprint(re.match(regexp_url, 'https://mail.google.com/mail/ca/u/0/#inbox').groupdict())
{'hostname': 'mail.google.com',
 'password': None,
 'path': '/mail/ca/u/0/#inbox',
 'port': None,
 'proto': 'https',
 'user': None}
>>> pprint(re.match(regexp_url, 'http://un-site:8080/greenhis/issues/14431').groupdict())
{'hostname': 'un-site',
 'password': None,
 'path': '/greenhis/issues/14431',
 'port': '8080',
 'proto': 'http',
 'user': None}
>>> pprint(re.match(regexp_url, 'https://docs.python.org/2/library/re.html#regular-expression-syntax').groupdict())
{'hostname': 'docs.python.org',
 'password': None,
 'path': '/2/library/re.html#regular-expression-syntax',
 'port': None,
 'proto': 'https',
 'user': None}
>>> pprint(re.match(regexp_url, 'ftp://toto:1234@ftpperso.free.fr/').groupdict())
{'hostname': 'ftpperso.free.fr',
 'password': '1234',
 'path': '/',
 'port': None,
 'proto': 'ftp',
 'user': 'toto'}
>>> pprint(re.match(regexp_url, 'ftp://toto:1234@ftpperso.free.fr:21/').groupdict())
{'hostname': 'ftpperso.free.fr',
 'password': '1234',
 'path': '/',
 'port': '21',
 'proto': 'ftp',
 'user': 'toto'}
>>> pprint(re.match(regexp_url, 'ftp://toto@ftpperso.free.fr:21/').groupdict())
{'hostname': 'ftpperso.free.fr',
 'password': None,
 'path': '/',
 'port': '21',
 'proto': 'ftp',
 'user': 'toto'}
>>>

Penser à partager vos améliorations si besoin.

Les fonctions urlparse et urlsplit de la classe parse du module urllib permettent aproximativement de faire la même chose

>>> import urllib
>>> urllib.parse.urlparse('http://un-site:8080/greenhis/issues/14431')
ParseResult(scheme='http', netloc='un-site:8080', path='/greenhis/issues/14431', params='', query='', fragment='')
>>> urllib.parse.urlparse('ftp://toto:1234@ftpperso.free.fr:21/')
ParseResult(scheme='ftp', netloc='toto:1234@ftpperso.free.fr:21', path='/', params='', query='', fragment='')
>>> urllib.parse.urlsplit('ftp://toto:1234@ftpperso.free.fr:21/')
SplitResult(scheme='ftp', netloc='toto:1234@ftpperso.free.fr:21', path='/', query='', fragment='')

 

Python: extraire les données d'une chaine de texte structurée en fonction d'une liste d'index

Compatible Python2 & Python3

Exemple avec le contenu de la variable "d" organisée sous la forme d'un tableau

>>> print(d)
2015  0 391371 374179 765550
2014  1 403204 385442 788646
2013  2 405502 386831 792333
2005 10 426532 406488 833020
2004 11 423605 404592 828197
2003 12 421432 402484 823916
1929 86  97489 180249 277738
1928 87  86122 165344 251466
1927 88  72603 149288 221891
1919 96   6011  21172  27183
1918 97   3649  13644  17293
1917 98   2013   9475  11488
1916 99   1324   6313   7637
>>>

Chaque ligne contient exactement le même nombre de données.
Chaque donnée (de longueur fixe) se trouve exactement à la même place sur chaque ligne.
Dans l'exemple, chaque ligne contient donc 5 colonnes.

>>> print(columns)
['Année de naissance', 'Age révolu', "Nombre d'hommes", 'Nombre de femmes', 'Ensemble']
>>>

Les données de la première colonne commencent à l'index 0
Les données de la seconde colonne commencent à l'index 5
Les données de la troisième colonne commencent à l'index 8
Les données de la quatrième colonne commencent à l'index 15
Et enfin, les données de la cinquième colonne commencent à l'index 22

Et c'est pareil pour chaque ligne.

>>> print(i)
[0, 5, 8, 15, 22]
>>>

Une petite boucle va permettre d'extraire toutes les données en fonction de la liste d'index ci-dessus

>>> for line in d.split('\n'):
    x = 0
    for pos in i:
        print("{COLUMN:>20s} : {DATA:<6s}".format(COLUMN=columns[x], DATA=line[pos:i[i.index(pos)+1] if i.index(pos)+1 < len(i) else None].strip()))
        x += 1
    print('*' * 31)

    
  Année de naissance : 2015  
          Age révolu : 0     
     Nombre d'hommes : 391371
    Nombre de femmes : 374179
            Ensemble : 765550
*******************************
  Année de naissance : 2014  
          Age révolu : 1     
     Nombre d'hommes : 403204
    Nombre de femmes : 385442
            Ensemble : 788646
*******************************
  Année de naissance : 2013  
          Age révolu : 2     
     Nombre d'hommes : 405502
    Nombre de femmes : 386831
            Ensemble : 792333
*******************************
  Année de naissance : 2005  
          Age révolu : 10    
     Nombre d'hommes : 426532
    Nombre de femmes : 406488
            Ensemble : 833020
*******************************
  Année de naissance : 2004  
          Age révolu : 11    
     Nombre d'hommes : 423605
    Nombre de femmes : 404592
            Ensemble : 828197
*******************************
  Année de naissance : 2003  
          Age révolu : 12    
     Nombre d'hommes : 421432
    Nombre de femmes : 402484
            Ensemble : 823916
*******************************
  Année de naissance : 1929  
          Age révolu : 86    
     Nombre d'hommes : 97489
    Nombre de femmes : 180249
            Ensemble : 277738
*******************************
  Année de naissance : 1928  
          Age révolu : 87    
     Nombre d'hommes : 86122
    Nombre de femmes : 165344
            Ensemble : 251466
*******************************
  Année de naissance : 1927  
          Age révolu : 88    
     Nombre d'hommes : 72603
    Nombre de femmes : 149288
            Ensemble : 221891
*******************************
  Année de naissance : 1919  
          Age révolu : 96    
     Nombre d'hommes : 6011  
    Nombre de femmes : 21172
            Ensemble : 27183
*******************************
  Année de naissance : 1918  
          Age révolu : 97    
     Nombre d'hommes : 3649  
    Nombre de femmes : 13644
            Ensemble : 17293
*******************************
  Année de naissance : 1917  
          Age révolu : 98    
     Nombre d'hommes : 2013  
    Nombre de femmes : 9475  
            Ensemble : 11488
*******************************
  Année de naissance : 1916  
          Age révolu : 99    
     Nombre d'hommes : 1324  
    Nombre de femmes : 6313  
            Ensemble : 7637  
*******************************
>>>

 

Python: glances (htop amélioré)

glances est un module python qui permet d'afficher un état détaillé du système, rafraichit automatiquement, à la manière de la commande htop.

Pré requis:

$ apt-get install python
$ apt-get install python-dev
$ apt-get install python-pip
$ pip install psutil

Pour l'installer:

$ pip install glances

Pour les mises à jour:

$ pip install psutil --upgrade
$ pip install glances --upgrade

Pour l'exécuter:

$ glances

Résultat:

Vraiment très pratique.

Python: obtenir le fuseau horaire à partir de coordonnées géographiques

Comment connaitre le fuseau horaire à partir d'une latitude et d'une longitude ?

Grâce au module timezonefinder.

# python3 -m pip install --upgrade timezonefinder

Et comment fonctionne-t-il ?

Tout simplement de cette manière:

Par exemple, avec les coordonnées de Montréal (Canada)

>>> from timezonefinder import TimezoneFinder
>>> timezoneid = TimezoneFinder().timezone_at(lat=45.5704, lng=-73.7674)
>>> timezoneid
'America/Toronto'

Avec les coordonnées de Tokyo (Japon)

>>> timezoneid = TimezoneFinder().timezone_at(lat=35.876, lng=136.912)
>>> timezoneid
'Asia/Tokyo'

Avec les coordonnées de Nantes (France)

>>> timezoneid = TimezoneFinder().timezone_at(lat=47.2383, lng=-1.5603)
>>> timezoneid
'Europe/Paris'

 

Python: pydf (pour controler l'espace disque)

pydf est un module python qui permet d'afficher à l'écran des statistiques sur l'espace disque (version évoluée de la commande df).

Pré requis:

$ apt-get install python
$ apt-get install python-dev
$ apt-get install python-pip

Pour l'installer:

$ pip install pydf

Pour les mises à jour:

$ pip install pydf --upgrade

Pour l'exécuter:

$ pydf

Résultat:

Plutôt sympa et vraiment très utile.

Python: pytail (tail -f à la mode Python)

Juste comme ça, pour s'amuser.

Voici un script Python 'pytail.py' qui fait exactement la même chose que la commande tail -f /path/to/log/file sous Linux, sauf que ce script peut-être utilisé sur tous les systèmes où la commande tail -f fait défaut (comme sous Windows par exemple).

Testé avec Python 3

#!/usr/bin/env python3

import re, os, time, sys
from time import time, sleep
from os.path import join, getsize, getmtime

if len(sys.argv) == 1:
    print("Argument Missing !")
    print("Usage:\n    {0:s} /path/to/log/file".format(sys.argv[0]))
    sys.exit(1)

startTime = time()
mTimeStart = 0
fileName = sys.argv[1]

if not os.path.exists(fileName) or not os.path.isfile(fileName):
    print("This file '{0:s}' does not exist !".format(fileName))
    sys.exit(2)

try:
    with open(fileName, 'r') as f:
        f.read(1)
except Exception as e:
    print("This file '{0:s}' cannot be opened for reading !".format(fileName))
    sys.exit(3)

folderLogs = os.path.dirname(fileName)

copyLines = []

try:

    while True:
        with open(fileName, 'r') as f:
            lines = f.readlines()

        show = False

        if len(copyLines) == 0:
            showLines = lines[-10:]
            show = True

        elif len(copyLines) != len(lines):
            showLines = lines[len(copyLines) - len(lines):]
            show = True

        if show:
            copyLines = lines.copy()
            for line in showLines:
                print(line, end='')

        sleep(1)

except KeyboardInterrupt as e:
    print("Program stopped by user !")
    sys.exit(0)

except Exception as e:
    print("Unknown error during execution !")
    print(e)
    sys.exit(4)

Pour l'exécuter:

# python3 pytail.py /var/log/mail.log

Merci qui, merci Python.

Etiquettes: