Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.
Application
Cabinet vétérinaire, jointures SQL et dernières vaccinations
Le cabinet veut préparer des rappels de vaccination par SMS, dans le cadre annuel défini par le sujet. Le travail porte sur le nettoyage des téléphones, leur validation, l’extraction des consultations puis la conservation de la dernière vaccination par chat. L’envoi de SMS et le filtrage final des rappels entre 10 et 13 mois constituent le contexte du projet ; ils ne font pas partie des quatre questions à programmer.
| Table | Champs | Relations |
|---|---|---|
| proprietaire | id (clé primaire), nom, prenom, telephone | Un propriétaire peut avoir plusieurs animaux |
| animal | id (clé primaire), espece, nom, date_naissance, id_proprietaire | id_proprietaire référence proprietaire.id |
| consultation | id (clé primaire), id_animal, date, motif | id_animal référence animal.id |
Les identifiants sont des entiers ; les autres champs sont des chaînes. Les dates utilisent AAAAMMJJ, ce qui permet de les comparer dans l’ordre lexicographique. Le schéma se lit consultation.id_animal → animal.id, puis animal.id_proprietaire → proprietaire.id. Extraits reproduits du PDF :
| proprietaire.id | nom | prenom | telephone |
|---|---|---|---|
| 1 | Duhamel | François | 06-57-84-52-41 |
| 2 | Dumont | Marcelle | 0648747223 |
| 3 | Joubert | Anouk | 0 4 76 33 78 15 |
| animal.id | espece | nom | date_naissance | id_proprietaire |
|---|---|---|---|---|
| 1 | oiseau | Flocon | 20171111 | 1 |
| 2 | lapin | Roudoudou | 20210901 | 2 |
| 3 | chat | Mistral | 20251206 | 2 |
| consultation.id | id_animal | date | motif |
|---|---|---|---|
| 1 | 1 | 20171229 | vaccination |
| 25 | 4 | 20050621 | bilan |
| 34 | 5 | 20131106 | maladie |
sqlite 3 fait partie de la bibliothèque standard. Pour une requête paramétrée, le texte SQL contient ? et execute reçoit un tuple de paramètres, par exemple (2,) ou (date,). Le numéro mobile attendu comporte exactement 10 chiffres et commence par 06 ou 07. Le PDF comporte un X surnuméraire dans un des patrons : la longueur 10 et les tests lèvent l’ambiguïté.
Le dossier comporte le sujet et les fichiers listés dans les téléchargements. Les codes de départ sont reproduits ci-dessous. Travaillez sur une copie. Les appels professeur signalés dans les questions servent à présenter votre démarche ; vous pouvez aussi demander de l’aide en cas de difficulté.
Code fourni : veto.py
# -----------------------------------------------------------------------------
# Numéros de téléphones
# Question 1 : nettoyage des numéros de téléphone
# Écrire la fonction normalisation_tel ici
import sqlite3
def test_normalisation_tel():
"""
Tous les tests doivent passer...
"""
assert normalisation_tel("06 12 99 90 12") == "0612999012"
assert normalisation_tel("02 12 99 90 12") == "0212999012"
assert normalisation_tel("02.12.99.90.12") == "0212999012"
assert normalisation_tel("0.6.12.99.90.12") == "0612999012"
assert normalisation_tel("06-12-99-90-12") == "0612999012"
assert normalisation_tel("(0)6.12.99-90-12 gilbert") == "0612999012"
assert normalisation_tel("061299901") == "061299901"
assert normalisation_tel("06129990123") == "06129990123"
print('Les tests de la fonction normalisation_tel sont passés')
# -----------------------------------------------------------------------------
# Question 2 : validation des numéros de téléphone
def validation_tel(tel):
"""
Validation des numéros de téléphone portable français
selon les conditions spécifiées.
:param tel: numéro de téléphone
:return: True si le numéro est valide, False sinon
"""
if len(tel) != 10:
return False
if tel[0] != "0":
return False
if tel[1] != "6" and tel[1] != "7":
return False
return True
# Ecrire votre jeu de tests permettant
# de vérifier le bon fonctionnement de la fonction.
# -----------------------------------------------------------------------------
# Détermination de la liste des chats à vacciner
# Question 3 : interrogation de la base de données
DB_PATH = "cabinet.sqlite"
def proprietaires_animaux_nes_apres(date):
"""
Renvoie les noms et prénoms des propriétaires d'animaux nés après la date `date`,
triés par ordre alphabétique de noms puis prénoms
:param: date: date de naissance minimale
:return: liste [(nom_proprietaire, prenom_ proprietaire)]
"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
resultat = cursor.execute(
"""
SELECT proprietaire.nom, proprietaire.prenom
FROM proprietaire
JOIN animal ON proprietaire.id = animal.id_proprietaire
WHERE animal.date_naissance > ?
ORDER BY proprietaire.nom, proprietaire.prenom;
""",
(date,),
)
return list(resultat)
def consultation_vaccination_chat(date):
"""
Renvoie les consultations de vaccination de chats dont la date
est supérieure à la date `date`.
:param: date: date minimale
:return: liste [(id_animal, nom_animal, tel_proprietaire, date_consultation)]
"""
pass
def test_consultation_vaccination_chat():
vaccinations = consultation_vaccination_chat("20240923")
assert len(vaccinations) == 118
assert vaccinations[0] == (16, "Plume", "0.6.36.96.89.83", "20241024")
assert vaccinations[1] == (16, "Plume", "0.6.36.96.89.83", "20251125")
assert vaccinations[2] == (17, "Gollum", "0.6.36.96.89.83", "20250113")
assert vaccinations[3] == (26, "Olympe", "(0)4 73 98 01 23", "20250109")
assert vaccinations[4] == (32, "Chopin", "06.37.97.66.64", "20241201")
assert vaccinations[5] == (32, "Chopin", "06.37.97.66.64", "20251119")
assert vaccinations[6] == (34, "Jazz", "0.6.37.51.65.52", "20250801")
assert vaccinations[7] == (35, "Tango", "0324182", "20250706")
assert vaccinations[8] == (38, "Loulou", "05-35-95-87-54", "20250209")
# Vérification stricte du tri (ORDER BY id_animal, date_consultation)
assert vaccinations == sorted(vaccinations, key=lambda x: (x[0], x[3]))
print('Les tests de la fonction consultation_vaccination_chat sont passés')
# test_consultation_vaccination_chat()
# -----------------------------------------------------------------------------
# Question 4 : détermination de la date de dernière vaccination
def derniere_vaccination(consultations):
"""
Renvoie un dictionnaire ayant pour clef l'identifiant de l'animal,
et dont la valeur associée est la dernière consultation de cet animal.
Chaque consultation est un tuple :
(id_animal, nom_animal, tel_proprietaire, date_consultation)
"""
derniere = {}
for consult in consultations:
id_animal = consult[0]
date = consult[3]
if id_animal not in derniere:
derniere[id_animal] = consult
elif date < derniere[id_animal][3]:
derniere[id_animal] = consult
return derniere
def test_derniere_vaccination():
consultations_pour_test = [
(16, "Plume", "0.6.36.96.89.83", "20241024"),
(16, "Plume", "0.6.36.96.89.83", "20251125"),
(17, "Gollum", "0.6.36.96.89.83", "20250113"),
(26, "Olympe", "(0)4 73 98 01 23", "20250109"),
(32, "Chopin", "06.37.97.66.64", "20241201"),
(32, "Chopin", "06.37.97.66.64", "20251119"),
(34, "Jazz", "0.6.37.51.65.52", "20250801"),
(35, "Tango", "0324182", "20250706"),
(38, "Loulou", "05-35-95-87-54", "20250209"),
(39, "Tango", "07.45.48.02.42", "20250329"),
(40, "Sésame", "07.45.48.02.42", "20250228"),
(41, "Pixel", "0130709285", "20241204"),
(41, "Pixel", "0130709285", "20251222"),
]
resultat = derniere_vaccination(consultations_pour_test)
# Affichage des résultats, décommenter si besoin
# for a,r in resultat.items():
# print(a, ":", r)
assert derniere_vaccination(consultations_pour_test) == {
16: (16, "Plume", "0.6.36.96.89.83", "20251125"),
17: (17, "Gollum", "0.6.36.96.89.83", "20250113"),
26: (26, "Olympe", "(0)4 73 98 01 23", "20250109"),
32: (32, "Chopin", "06.37.97.66.64", "20251119"),
34: (34, "Jazz", "0.6.37.51.65.52", "20250801"),
35: (35, "Tango", "0324182", "20250706"),
38: (38, "Loulou", "05-35-95-87-54", "20250209"),
39: (39, "Tango", "07.45.48.02.42", "20250329"),
40: (40, "Sésame", "07.45.48.02.42", "20250228"),
41: (41, "Pixel", "0130709285", "20251222"),
}
print('Les tests de la fonction derniere_vaccination sont passés')
Question 1
#Écrire normalisation_tel(tel), qui ne conserve que les chiffres. Exemples : "(0)2 12 99 90 12" donne 0212999012, "0.6.12.99.90.12" donne 0612999012 et "0-6-12-9-90-1" donne 06129901. Utiliser le test fourni.
Indice
Tester chaque caractère avec isdigit.
Comprendre la correction
def normalisation_tel(tel):
resultat = ''
for caractere in tel:
if caractere.isdigit():
resultat += caractere
return resultatOn filtre les caractères sans modifier l’ordre ni compléter un numéro trop court. Le résultat reste une chaîne pour préserver le zéro initial. Cette étape ne décide pas encore si le numéro est un mobile valide : elle prépare la validation.
test_normalisation_tel()
assert normalisation_tel('') == ''
assert normalisation_tel('pas de numéro') == ''Question 2
#Écrire un jeu de tests vérifiant validation_tel sur un numéro déjà nettoyé. Il doit contenir 10 chiffres et commencer par 06 ou 07.
Comprendre la correction
def test_validation_tel():
assert validation_tel('0612999012')
assert validation_tel('0712999012')
assert not validation_tel('0212999012') # fixe
assert not validation_tel('1612999012') # premier chiffre
assert not validation_tel('061299901') # neuf chiffres
assert not validation_tel('06129990123') # onze chiffres
assert not validation_tel('') # liste vide de caractères
test_validation_tel()On couvre chaque condition et les deux préfixes autorisés. Le contrat précise un numéro déjà nettoyé ; le code fourni ne revérifie donc pas que chaque caractère est un chiffre. Pour une fonction destinée à des entrées arbitraires, on ajouterait une validation ASCII explicite, mais il ne faut pas mélanger ce changement avec les tests du contrat demandé.
Question 3
#Écrire consultation_vaccination_chat(date) : renvoyer les consultations de motif vaccination pour l’espèce chat, à une date strictement supérieure au paramètre, avec id animal, nom animal, téléphone propriétaire et date, triés par id puis date croissants. Passer les tests fournis.
SQLExécuter la requête des vaccinations de chatsÉcrivez votre solution et mettez-la à l’épreuve
Travaillez uniquement la partie SQL de consultation_vaccination_chat pour l’appel avec la date 20240923. Renvoyez animal.id, animal.nom, proprietaire.telephone brut et consultation.date, pour les chats vaccinés après cette date strictement. Triez par animal.id, puis consultation.date croissants. Dans cet atelier SQL, écrivez la date '20240923' comme littéral à la place du paramètre Python.
SELECT ...
FROM ...
WHERE ...;Les cas de test proposés :
- Sous-ensemble de cabinet.sqlite fourni : Schéma extrait directement de
sqlite_masteret lignes lues dans le fichiercabinet.sqlitefourni. Les téléphones sont conservés tels quels ; leur nettoyage appartient à une autre question. - Cas complémentaire : date stricte, espèce et motif : Jeu complémentaire pédagogique, distinct des données officielles. La date exactement égale au seuil est exclue. Une consultation de bilan et la vaccination d’un chien ne correspondent pas au filtre.
- Cas complémentaire : deux consultations distinctes le même jour : Jeu complémentaire pédagogique, distinct des données officielles. La base identifie une consultation par son
id, pas par son couple animal/date. La requête demandée conserve donc les deux lignes, avant la sélection d’une dernière vaccination à la question suivante.
Indice
Partir de animal, rejoindre son propriétaire et ses consultations.
Comprendre la correction
def consultation_vaccination_chat(date):
conn = sqlite3.connect(DB_PATH)
try:
cursor = conn.cursor()
resultat = cursor.execute("""
SELECT animal.id, animal.nom, proprietaire.telephone, consultation.date
FROM animal
JOIN proprietaire ON animal.id_proprietaire = proprietaire.id
JOIN consultation ON consultation.id_animal = animal.id
WHERE animal.espece = 'chat'
AND consultation.motif = 'vaccination'
AND consultation.date > ?
ORDER BY animal.id, consultation.date
""", (date,))
return list(resultat)
finally:
conn.close()Deux jointures relient les trois tables par leurs clés. Le WHERE combine trois restrictions indépendantes. La date reste un paramètre : ne pas la concaténer au SQL. La virgule dans (date,) construit un tuple à un seul élément. La conversion list est effectuée avant de fermer la connexion.
test_consultation_vaccination_chat()
print(consultation_vaccination_chat('20240923')[:3])| id | nom | téléphone | date |
|---|---|---|---|
| 16 | Plume | 0.6.36.96.89.83 | 20241024 |
| 16 | Plume | 0.6.36.96.89.83 | 20251125 |
| 17 | Gollum | 0.6.36.96.89.83 | 20250113 |
La base réellement fournie renvoie 118 consultations pour 20240923, dans l’ordre attendu. Les téléphones restent bruts dans cette requête, comme le demandent ses tests ; le nettoyage est une étape distincte.
Vérifiez la requête en suivant un résultat. La consultation porte un identifiant d’animal ; la première relation retrouve le chat et son nom ; l’animal porte l’identifiant de son propriétaire ; la seconde relation retrouve le téléphone. Un propriétaire peut avoir plusieurs animaux et un animal plusieurs consultations : il est donc normal de voir plusieurs lignes avec le même téléphone ou le même chat. Ajouter DISTINCT au hasard risquerait de masquer cette structure. Le tri par identifiant puis date prépare la lecture, mais ne remplace pas encore la sélection de la dernière vaccination.
Question 4
#Expliquer pourquoi derniere_vaccination ne passe pas ses tests et corriger. Pour Plume, conserver 20251125 plutôt que 20241024 ; pour Gollum, conserver 20250113.
PythonRetenir la dernière vaccination dans n’importe quel ordreÉcrivez votre solution et mettez-la à l’épreuve
Corrigez derniere_vaccination(consultations). Chaque tuple contient (id_animal, nom, téléphone, date au format AAAAMMJJ). Renvoyez un dictionnaire associant chaque identifiant au tuple complet de sa consultation la plus récente. Les dates de longueur fixe sont comparables comme des chaînes ; les entrées ne sont pas forcément triées.
def derniere_vaccination(consultations):
# À vous de jouer
pass
Les cas de test proposés :
- Plume et Gollum : Chaque animal conserve sa consultation la plus récente.
- Plus récent en premier : Écraser à chaque passage ne fonctionne pas sans tri garanti.
- Deux animaux de même nom : La clé est l’identifiant, pas le nom.
- Le téléphone vient du tuple retenu : On conserve toute la ligne de la consultation gagnante.
- Liste vide et entrée intacte : Regrouper ne demande pas de modifier la liste de consultations.
Indice
Dernière signifie maximum de date.
Comprendre la correction
La comparaison date<date_actuelle conserve la vaccination la plus ancienne. On cherche la plus récente, donc le maximum : il faut inverser le comparateur. La première consultation de chaque animal initialise son entrée, puis les suivantes ne la remplacent que si elles sont postérieures.
def derniere_vaccination(consultations):
derniere = {}
for consult in consultations:
identifiant = consult[0]
if identifiant not in derniere or consult[3] > derniere[identifiant][3]:
derniere[identifiant] = consult
return dernieretest_derniere_vaccination()
assert derniere_vaccination([]) == {}
cas=[(16,'Plume','x','20251125'),(16,'Plume','x','20241024')]
assert derniere_vaccination(cas)[16][3] == '20251125'Cette version fonctionne aussi lorsque les consultations arrivent dans un ordre quelconque. Un simple écrasement à chaque passage ne serait correct qu’avec un tri chronologique croissant garanti. La troisième composante est le téléphone dans le code et les exemples, malgré une phrase du PDF qui la nomme id_proprietaire.
Après la première consultation de Plume, le dictionnaire associe 16 au tuple daté 20241024. À la deuxième, la date 20251125 est plus grande, donc le tuple entier remplace l’ancien. Le passage à Gollum crée une autre clé sans toucher celle de Plume. La propriété à conserver après chaque étape est : pour chaque animal déjà rencontré, le dictionnaire contient sa consultation la plus récente parmi celles déjà lues. Un ordre inversé des entrées est un bon test : le maximum doit rester le même.
Jointure et dernier enregistrementUn atelier pour expérimenter
Choisissez une réponse puis confrontez votre intuition à l’explication. Vous pouvez modifier vos choix.
Lire les réponses et leurs explications
- Quelle clé relie consultation à animal ?
consultation.id_animal=animal.id. La clé étrangère désigne précisément l’animal.
- Pourquoi le maximum sur AAAAMMJJ fonctionne-t-il ?
Le format fixe place les unités les plus grandes d’abord.. Année, mois puis jour reproduisent l’ordre chronologique.
Appuyez votre explication sur les valeurs et les conditions exactes du programme.
Observer l’accumulation de la dernière vaccinationUn atelier pour expérimenter
Choisissez l’ordre des trois consultations de Plume et Gollum, puis le comparateur. Le tableau montre le dictionnaire après chaque traitement.
Lire le résultat de l’expérience initiale
Dernière date stockée pour Plume : 20251125.
Le sens du comparateur choisit un minimum ou un maximum. L’ordre d’entrée ne change pas le maximum final.
| Animal | Date lue | Action | Dictionnaire courant |
|---|---|---|---|
| Plume | 20241024 | Conserver/remplacer | 16: 20241024 |
| Plume | 20251125 | Conserver/remplacer | 16: 20251125 |
| Gollum | 20250113 | Conserver/remplacer | 16: 20251125 ; 17: 20250113 |
Un dictionnaire peut conserver le meilleur enregistrement de chaque groupe en un seul parcours.
Du sujet à la méthode
Votre prochaine séance de révision
- Séparer nettoyage, validation et requête de sélection.
- Justifier les clés de jointure et le sens du comparateur de date.
Retrouver ces notions dans d’autres sujets
Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027
Énoncé : sujet 26_BCG_NSI_15 (PDF). Corrigé et explications pédagogiques proposés par Sofien.
