diff --git a/README.md b/README.md deleted file mode 100644 index 5b55bc4..0000000 --- a/README.md +++ /dev/null @@ -1,199 +0,0 @@ -# Système d'Analyse de Tickets de Support - -Ce système d'analyse de tickets de support permet de traiter les données des tickets pour en extraire des informations pertinentes, analyser les images, et générer des rapports d'analyse. - -## Architecture - -Le système est désormais structuré de manière modulaire, avec des étapes de traitement distinctes qui peuvent être exécutées indépendamment: - -1. **Extraction des données** (`extract_ticket.py`) - Nettoie et prépare les données brutes des tickets -2. **Filtrage des images** (`filter_images.py`) - Identifie les images pertinentes dans les pièces jointes -3. **Analyse d'images** (`analyze_image_contexte.py`) - Analyse les images pertinentes en fonction du contexte -4. **Analyse de ticket** (`analyze_ticket.py`) - Analyse le contenu du ticket pour en extraire les informations clés -5. **Questions-Réponses** (`extract_question_reponse.py`) - Extrait les paires de questions et réponses du ticket - -Ces étapes peuvent être exécutées individuellement ou dans une séquence complète via le script principal (`processus_complet.py`). - -## Prérequis - -- Python 3.9+ -- Bibliothèques requises (listées dans `requirements.txt`) -- Clé API pour les modèles de langage utilisés (configurée dans `config.json`) - -## Installation - -```bash -# Cloner le dépôt -git clone -cd - -# Installer les dépendances -pip install -r requirements.txt - -# Configurer la clé API -cp config.json.example config.json -# Éditer config.json pour ajouter votre clé API -``` - -## Configuration - -Créez un fichier `config.json` à la racine du projet avec le contenu suivant: - -```json -{ - "llm": { - "api_key": "votre-clé-api-ici", - "api_base": "https://api.mistral.ai/v1", - "organization": "votre-organisation" - } -} -``` - -## Utilisation - -### Processus complet - -Pour exécuter l'ensemble du processus d'analyse sur un ticket: - -```bash -python scripts/processus_complet.py --ticket T0167 -``` - -Options disponibles: -- `--ticket` ou `-t`: Code du ticket à analyser (obligatoire) -- `--source` ou `-s`: Dossier source contenant les tickets bruts (par défaut: `output/`) -- `--output` ou `-o`: Dossier de sortie pour les résultats (par défaut: `output_processed/`) -- `--verbose` ou `-v`: Afficher plus d'informations - -### Étapes individuelles - -Vous pouvez exécuter uniquement une étape spécifique: - -```bash -python scripts/processus_complet.py --ticket T0167 --etapes extraction -``` - -Étapes disponibles: -- `extraction`: Extraction et nettoyage des données du ticket -- `filtrage`: Filtrage des images pertinentes -- `analyse_images`: Analyse des images pertinentes -- `analyse_ticket`: Analyse du contenu du ticket -- `questions_reponses`: Extraction des questions et réponses -- `tout`: Exécute toutes les étapes (par défaut) - -### Scripts individuels - -Vous pouvez aussi exécuter directement les scripts individuels pour plus de contrôle: - -#### 1. Extraction des données - -```bash -python scripts/extract_ticket.py output/ticket_T0167 --output-dir output_processed/ticket_T0167 -``` - -#### 2. Filtrage des images - -```bash -python scripts/filter_images.py --dossier-ticket output_processed/ticket_T0167 -``` - -#### 3. Analyse d'images - -```bash -python scripts/analyze_image_contexte.py --image chemin/vers/image.jpg --ticket-info output_processed/ticket_T0167/ticket_info.json -``` - -#### 4. Analyse de ticket - -```bash -python scripts/analyze_ticket.py --messages output_processed/ticket_T0167/messages.json --images-rapport output_processed/ticket_T0167/filter_report.json -``` - -#### 5. Questions-Réponses - -```bash -python scripts/extract_question_reponse.py --messages output_processed/ticket_T0167/messages.json -``` - -## Structure des dossiers - -``` -. -├── config.json # Configuration (clés API, etc.) -├── main.py # Script principal original (pour compatibilité) -├── post_process.py # Post-traitement original (pour compatibilité) -├── requirements.txt # Dépendances du projet -├── scripts/ # Scripts modulaires -│ ├── analyze_image_contexte.py # Analyse d'images avec contexte -│ ├── analyze_ticket.py # Analyse de ticket -│ ├── extract_question_reponse.py # Extraction de questions-réponses -│ ├── extract_ticket.py # Extraction et nettoyage de données -│ ├── filter_images.py # Filtrage d'images -│ └── processus_complet.py # Orchestration du processus complet -├── output/ # Données brutes des tickets -│ └── ticket_TXXXX/ # Dossier d'un ticket brut -├── output_processed/ # Données traitées et résultats -│ └── ticket_TXXXX/ # Dossier d'un ticket traité -│ ├── messages.json # Messages nettoyés -│ ├── ticket_info.json # Informations du ticket -│ ├── attachments/ # Pièces jointes -│ ├── filter_report.json # Rapport de filtrage d'images -│ ├── images_analyses/ # Analyses d'images -│ ├── questions_reponses.md # Questions et réponses extraites -│ └── rapport/ # Rapports d'analyse -├── agents/ # Agents d'analyse (pour compatibilité) -├── llm/ # Interfaces avec les modèles de langage -└── utils/ # Utilitaires communs -``` - -## Dépannage - -### Problèmes courants - -1. **Messages non traités correctement**: - - Exécutez `extract_ticket.py` avec l'option `--verbose` pour voir les détails du traitement - - Vérifiez que le fichier messages.json est correctement formaté - -2. **Images non détectées**: - - Assurez-vous que les images sont dans le dossier `attachments/` - - Vérifiez les formats d'image supportés (.jpg, .png, .gif, etc.) - -3. **Erreurs LLM**: - - Vérifiez que votre clé API est valide et correctement configurée dans `config.json` - - Assurez-vous d'avoir une connexion internet stable - -### Journaux - -Chaque script génère un fichier de journal dans le répertoire de travail: -- `extract_ticket.log` -- `filter_images.log` -- `analyze_image.log` -- `analyze_ticket.log` -- `extract_qr.log` -- `processus_complet.log` - -Consultez ces fichiers pour plus de détails sur les erreurs rencontrées. - -## Exemples - -### Exemple 1: Analyser un ticket complet - -```bash -python scripts/processus_complet.py --ticket T0167 --verbose -``` - -### Exemple 2: Extraire uniquement les questions-réponses - -```bash -python scripts/extract_question_reponse.py --messages output/ticket_T0167/messages.json --output output/ticket_T0167/questions_reponses.md -``` - -### Exemple 3: Réanalyser un ticket avec des changements - -```bash -# Nettoyer d'abord les données -python scripts/extract_ticket.py output/ticket_T0167 --output-dir output_processed/ticket_T0167 - -# Puis extraire les questions-réponses -python scripts/extract_question_reponse.py --messages output_processed/ticket_T0167/messages.json -``` \ No newline at end of file diff --git a/__pycache__/post_process.cpython-312.pyc b/__pycache__/post_process.cpython-312.pyc deleted file mode 100644 index 0b0e2be..0000000 Binary files a/__pycache__/post_process.cpython-312.pyc and /dev/null differ diff --git a/agents/__init__.py b/agents/__init__.py deleted file mode 100644 index 86b5e18..0000000 --- a/agents/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from .agent_base import Agent -from .agent_filtre_images import AgentFiltreImages -from .agent_analyse_image import AgentAnalyseImage -from .agent_question_reponse import AgentQuestionReponse - -__all__ = [ - 'Agent', - 'AgentFiltreImages', - 'AgentAnalyseImage', - 'AgentQuestionReponse' -] \ No newline at end of file diff --git a/agents/__pycache__/__init__.cpython-312.pyc b/agents/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 35edb3f..0000000 Binary files a/agents/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_analyse_image.cpython-312.pyc b/agents/__pycache__/agent_analyse_image.cpython-312.pyc deleted file mode 100644 index d1340fc..0000000 Binary files a/agents/__pycache__/agent_analyse_image.cpython-312.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_base.cpython-312.pyc b/agents/__pycache__/agent_base.cpython-312.pyc deleted file mode 100644 index f156114..0000000 Binary files a/agents/__pycache__/agent_base.cpython-312.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_filtre_images.cpython-312.pyc b/agents/__pycache__/agent_filtre_images.cpython-312.pyc deleted file mode 100644 index 6b1c292..0000000 Binary files a/agents/__pycache__/agent_filtre_images.cpython-312.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_question_reponse.cpython-312.pyc b/agents/__pycache__/agent_question_reponse.cpython-312.pyc deleted file mode 100644 index d75c4f2..0000000 Binary files a/agents/__pycache__/agent_question_reponse.cpython-312.pyc and /dev/null differ diff --git a/agents/agent_analyse_image.py b/agents/agent_analyse_image.py deleted file mode 100644 index 5933e44..0000000 --- a/agents/agent_analyse_image.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Agent pour analyser en détail les images pertinentes dans un contexte de support technique. -""" - -import os -from typing import Dict, Any, Optional - -from .agent_base import Agent -from llm import Pixtral - -class AgentAnalyseImage(Agent): - """ - Agent qui analyse en détail le contenu d'une image technique. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent d'analyse d'images. - - Args: - api_key: Clé API pour le LLM - """ - super().__init__("AgentAnalyseImage") - self.llm = Pixtral(api_key=api_key) - self.llm.system_prompt = """ -Vous êtes un expert en analyse d'images techniques. -Votre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de: -1. Décrire précisément le contenu de l'image -2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations) -3. Extraire tout texte visible (messages d'erreur, logs, indicateurs) - -Soyez précis et factuel dans votre analyse. -""" - - def executer(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]: - """ - Analyse en détail une image technique. - - Args: - image_path: Chemin vers l'image à analyser - contexte: Contexte optionnel du ticket pour une analyse plus pertinente - - Returns: - Dictionnaire contenant l'analyse de l'image - """ - # Vérifier que l'image existe - if not os.path.exists(image_path): - erreur = f"L'image {image_path} n'existe pas" - self.ajouter_historique("analyse_image_erreur", image_path, erreur) - return {"error": erreur} - - # Construire le prompt pour l'analyse - if contexte: - prompt = f""" -Analysez cette image dans le contexte suivant: - -{contexte} - -Décrivez précisément: -1. Ce que vous voyez dans l'image -2. Les éléments techniques visibles -3. Tout texte visible (messages d'erreur, logs, etc.) -4. Les indices sur le problème potentiel -""" - else: - prompt = """ -Analysez cette image en détail et décrivez ce que vous voyez. -Identifiez tout problème technique visible, messages d'erreur, et éléments importants. -""" - - # Enregistrer l'action dans l'historique - self.ajouter_historique("analyse_image", image_path, "Analyse en cours...") - - # Appel au LLM - try: - resultat = self.llm.analyze_image(image_path, prompt) - - # Vérifier si l'appel a réussi - if "error" in resultat: - self.ajouter_historique("analyse_image_erreur", image_path, resultat["error"]) - return resultat - - self.ajouter_historique("analyse_image_resultat", "Analyse terminée", - resultat.get("content", "")[:200]) - return resultat - except Exception as e: - erreur = f"Erreur lors de l'analyse: {str(e)}" - self.ajouter_historique("analyse_image_erreur", image_path, erreur) - return {"error": erreur} \ No newline at end of file diff --git a/agents/agent_base.py b/agents/agent_base.py deleted file mode 100644 index 580d659..0000000 --- a/agents/agent_base.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Module définissant la classe abstraite Agent qui servira de base pour tous les agents -spécialisés du système d'analyse de tickets. -""" - -import os -import json -from abc import ABC, abstractmethod -from datetime import datetime -from typing import Dict, List, Any, Optional - -class Agent(ABC): - """ - Classe abstraite définissant l'interface commune de tous les agents. - """ - def __init__(self, nom: str): - """ - Initialise un agent avec son nom et son historique d'actions. - - Args: - nom: Nom de l'agent - """ - self.nom: str = nom - self.historique: List[Dict[str, Any]] = [] - self.llm_params: Dict[str, Any] = {} - self.llm = None - - def ajouter_historique(self, action: str, input_data: Any, output_data: Any) -> None: - """ - Ajoute une entrée dans l'historique de l'agent. - - Args: - action: Type d'action réalisée - input_data: Données d'entrée (limitées pour économiser l'espace) - output_data: Données de sortie (limitées pour économiser l'espace) - """ - self.historique.append({ - "timestamp": datetime.now().isoformat(), - "action": action, - "input": str(input_data)[:500], # Limitation pour éviter des historiques trop volumineux - "output": str(output_data)[:500] # Limitation pour éviter des historiques trop volumineux - }) - - def obtenir_historique(self) -> List[Dict[str, Any]]: - """ - Retourne l'historique complet de l'agent. - - Returns: - Liste d'actions avec leurs timestamp et données - """ - return self.historique - - def sauvegarder_historique(self, chemin_fichier: str) -> bool: - """ - Sauvegarde l'historique de l'agent dans un fichier JSON. - - Args: - chemin_fichier: Chemin où sauvegarder le fichier JSON - - Returns: - True si la sauvegarde a réussi, False sinon - """ - try: - # Créer le répertoire parent si nécessaire - os.makedirs(os.path.dirname(chemin_fichier), exist_ok=True) - - with open(chemin_fichier, 'w', encoding='utf-8') as f: - json.dump(self.historique, f, ensure_ascii=False, indent=2) - return True - except Exception as e: - print(f"Erreur lors de la sauvegarde de l'historique: {e}") - return False - - def configurer_llm(self, **parametres: Any) -> None: - """ - Configure les paramètres du LLM associé à l'agent. - - Args: - **parametres: Paramètres à configurer (température, modèle, etc.) - """ - if self.llm is None: - raise ValueError("Aucun LLM associé à cet agent") - - # Enregistrer les paramètres modifiés - self.llm_params.update(parametres) - - # Appliquer les paramètres au LLM - for param, valeur in parametres.items(): - if hasattr(self.llm, param): - setattr(self.llm, param, valeur) - else: - print(f"Avertissement: Le paramètre '{param}' n'existe pas dans le LLM") - - # Ajouter à l'historique - self.ajouter_historique("configuration_llm", - f"Paramètres: {parametres}", - f"Paramètres actuels: {self.obtenir_parametres_llm()}") - - def obtenir_parametres_llm(self) -> Dict[str, Any]: - """ - Obtient les paramètres actuels du LLM associé à l'agent. - - Returns: - Dictionnaire des paramètres actuels du LLM - """ - if self.llm is None: - return {} - - # Paramètres à récupérer (étendre selon vos besoins) - params_keys = [ - "model", "temperature", "max_tokens", "top_p", - "frequency_penalty", "presence_penalty", "system_prompt" - ] - - # Extraire les valeurs des paramètres - params = {} - for key in params_keys: - if hasattr(self.llm, key): - params[key] = getattr(self.llm, key) - - return params - - def generer_rapport_parametres(self) -> Dict[str, Any]: - """ - Génère un rapport des paramètres utilisés par l'agent. - - Returns: - Dictionnaire contenant les informations sur les paramètres - """ - return { - "agent": self.nom, - "llm_type": self.llm.__class__.__name__ if self.llm else "Aucun", - "parametres": self.obtenir_parametres_llm(), - "parametres_modifies": self.llm_params - } - - def appliquer_parametres_globaux(self, params_globaux: Dict[str, Any]) -> None: - """ - Applique des paramètres globaux au LLM de l'agent. - Les paramètres spécifiques déjà définis ont priorité sur les globaux. - - Args: - params_globaux: Dictionnaire de paramètres globaux à appliquer - """ - if self.llm is None: - return - - # Filtrer les paramètres applicables - params_a_appliquer = {} - for param, valeur in params_globaux.items(): - # Ne pas écraser les paramètres déjà définis spécifiquement pour cet agent - if param not in self.llm_params and hasattr(self.llm, param): - params_a_appliquer[param] = valeur - - # Appliquer les paramètres filtrés - if params_a_appliquer: - self.configurer_llm(**params_a_appliquer) - self.ajouter_historique("application_params_globaux", - f"Paramètres globaux appliqués: {params_a_appliquer}", - f"Paramètres actuels: {self.obtenir_parametres_llm()}") - - @abstractmethod - def executer(self, *args, **kwargs) -> Dict[str, Any]: - """ - Méthode abstraite que chaque agent concret doit implémenter. - Cette méthode exécute la fonction principale de l'agent. - - Returns: - Dictionnaire contenant les résultats de l'exécution - """ - pass \ No newline at end of file diff --git a/agents/agent_filtre_images.py b/agents/agent_filtre_images.py deleted file mode 100644 index d03d4da..0000000 --- a/agents/agent_filtre_images.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Agent pour filtrer les images non pertinentes dans un contexte de support technique. -""" - -import os -import json -import re -from typing import Dict, Any, Optional - -from .agent_base import Agent -from llm import Pixtral - -class AgentFiltreImages(Agent): - """ - Agent qui détermine si une image est pertinente dans un contexte de support technique. - Filtre les logos, signatures, icônes, etc. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent de filtrage d'images. - - Args: - api_key: Clé API pour le LLM - """ - super().__init__("AgentFiltreImages") - self.llm = Pixtral(api_key=api_key) - - # Configuration par défaut du LLM - default_system_prompt = """ -Vous êtes un expert en analyse d'images techniques. Votre mission est de déterminer -si une image est pertinente dans un contexte de support technique ou non. - -Images PERTINENTES: -- Captures d'écran montrant des problèmes, erreurs, bugs -- Photos d'équipements avec problèmes visibles -- Schémas techniques ou diagrammes -- Graphiques de données techniques - -Images NON PERTINENTES: -- Logos d'entreprise -- Signatures ou avatars -- Icônes ou boutons isolés -- Bannières décoratives, séparateurs -- Images génériques sans information technique -""" - self.configurer_llm( - system_prompt=default_system_prompt, - temperature=0.2, # Basse température pour des réponses précises - max_tokens=500 # Réponses courtes pour le filtrage - ) - - def executer(self, image_path: str) -> Dict[str, Any]: - """ - Détermine si une image est pertinente pour l'analyse technique. - - Args: - image_path: Chemin vers l'image à analyser - - Returns: - Dictionnaire contenant le résultat du filtrage avec au minimum - {'pertinente': True/False} - """ - # Vérifier que l'image existe - if not os.path.exists(image_path): - erreur = f"L'image {image_path} n'existe pas" - self.ajouter_historique("filtre_image_erreur", image_path, erreur) - return {"pertinente": False, "error": erreur} - - # Construire le prompt pour la classification - prompt = """ -Analysez cette image et déterminez si elle est PERTINENTE ou NON PERTINENTE dans un contexte de support technique. - -Répondez au format JSON suivant: -{ - "pertinente": true/false, - "type_image": "capture_ecran|photo_equipement|schema|graphique|logo|icone|decorative|autre", - "description": "Brève description du contenu", - "confiance": "valeur de 0 à 100", - "justification": "Explication de votre décision" -} -""" - - # Enregistrer l'action dans l'historique - self.ajouter_historique("filtre_image", image_path, "Filtrage en cours...") - - # Appel au LLM - try: - # Pour les cas complexes, augmenter légèrement la température - if "complexe" in image_path or os.path.getsize(image_path) > 500000: - self.configurer_llm(temperature=0.4) - self.ajouter_historique("ajustement_temperature", "Augmentation pour image complexe", "temperature=0.4") - - resultat_brut = self.llm.analyze_image(image_path, prompt) - - # Essayer d'extraire le JSON de la réponse - try: - content = resultat_brut.get("content", "") - - # Si le contenu est déjà au format JSON correctement formaté - if content.strip().startswith("{") and content.strip().endswith("}"): - try: - resultat_json = json.loads(content) - except json.JSONDecodeError: - # Si le contenu a la structure JSON mais ne peut pas être décodé - # nettoyer et réessayer - content_cleaned = content.replace("```json", "").replace("```", "").strip() - resultat_json = json.loads(content_cleaned) - else: - # Chercher un bloc JSON dans la réponse - json_match = re.search(r'```json\s*(.*?)\s*```', content, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - else: - # Essayer d'extraire un objet JSON sans les blocs de code - json_match = re.search(r'(\{.*?\})', content, re.DOTALL) - if json_match: - json_str = json_match.group(1).strip() - else: - # Sinon, prendre tout le contenu comme JSON potentiel - json_str = content.strip() - - # Parser le JSON - resultat_json = json.loads(json_str) - - # S'assurer que le champ 'pertinente' existe - if "pertinente" not in resultat_json: - resultat_json["pertinente"] = False - resultat_json["error"] = "Format de réponse incorrect" - - # Ajouter les paramètres LLM utilisés - resultat_json["parametres_llm"] = self.generer_rapport_parametres() - - self.ajouter_historique("filtre_image_resultat", "Filtrage terminé", - f"Pertinente: {resultat_json.get('pertinente', False)}") - return resultat_json - - except Exception as e: - # Pour les modules en mode simulation, utiliser directement la réponse - if "pertinente" in resultat_brut.get("content", ""): - try: - resultat_json = json.loads(resultat_brut.get("content", "")) - resultat_json["parametres_llm"] = self.generer_rapport_parametres() - self.ajouter_historique("filtre_image_resultat", "Filtrage terminé", - f"Pertinente: {resultat_json.get('pertinente', False)}") - return resultat_json - except: - pass - - # En cas d'erreur de parsing, retourner un résultat par défaut - resultat = { - "pertinente": False, - "error": f"Erreur de parsing JSON: {str(e)}", - "response_raw": resultat_brut.get("content", "")[:200], - "parametres_llm": self.generer_rapport_parametres() - } - self.ajouter_historique("filtre_image_parsing_erreur", "Erreur de parsing", str(e)) - return resultat - - except Exception as e: - erreur = f"Erreur lors du filtrage: {str(e)}" - self.ajouter_historique("filtre_image_erreur", image_path, erreur) - return { - "pertinente": False, - "error": erreur, - "parametres_llm": self.generer_rapport_parametres() - } \ No newline at end of file diff --git a/agents/agent_question_reponse.py b/agents/agent_question_reponse.py deleted file mode 100644 index 36fc0e5..0000000 --- a/agents/agent_question_reponse.py +++ /dev/null @@ -1,474 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Agent pour extraire et structurer les questions et réponses des tickets de support. -""" - -import os -import re -from typing import Dict, List, Any, Optional - -from .agent_base import Agent -from llm import Mistral -from post_process import normaliser_accents - -class AgentQuestionReponse(Agent): - """ - Agent qui extrait les questions et réponses des messages de support. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent d'extraction de questions et réponses. - - Args: - api_key: Clé API pour le LLM - """ - super().__init__("AgentQuestionReponse") - self.llm = Mistral(api_key=api_key) - # Configuration par défaut du LLM - default_system_prompt = """ -Vous êtes un expert en analyse de conversations de support technique. - -Votre mission est d'identifier avec précision: -1. Le rôle de chaque intervenant (client ou support technique) -2. La nature de chaque message (question, réponse, information additionnelle) -3. Le contenu essentiel de chaque message en éliminant les formules de politesse, - signatures, mentions légales et autres éléments non pertinents - -Pour l'identification client/support: -- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email - comme @cbao.fr, @odoo.com, mentions "support technique", etc. -- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions - -Pour la classification en question/réponse: -- Questions: Demandes explicites (avec "?"), demandes implicites de résolution - de problèmes, descriptions de bugs ou dysfonctionnements -- Réponses: Explications techniques, solutions proposées, instructions fournies - par le support - -Concentrez-vous uniquement sur le contenu technique utile en ignorant tous les -éléments superflus qui n'apportent pas d'information sur le problème ou sa solution. -""" - self.configurer_llm( - system_prompt=default_system_prompt, - temperature=0.3, # Basse température pour une extraction précise - max_tokens=2000 - ) - - def _nettoyer_contenu(self, texte: str) -> str: - """ - Nettoie le contenu en supprimant signatures, mentions légales, etc. - - Args: - texte: Texte brut à nettoyer - - Returns: - Texte nettoyé des éléments non pertinents - """ - # Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide - if not isinstance(texte, str): - if texte is None: - return "" - try: - texte = str(texte) - except: - return "" - - # Détection de contenu HTML - contient_html = bool(re.search(r'<[a-z]+[^>]*>', texte, re.IGNORECASE)) - - # Supprimer les balises HTML - approche plus robuste - try: - # Première passe - balises standard - texte_nettoye = re.sub(r']*>', ' ', texte, flags=re.IGNORECASE) - - # Deuxième passe - balises restantes, y compris les mal formées - texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye) - - # Troisième passe pour les balises qui pourraient avoir échappé - texte_nettoye = re.sub(r'<[^>]*$', ' ', texte_nettoye) # Balises incomplètes à la fin - except Exception as e: - self.ajouter_historique("erreur_nettoyage_html", "Échec", str(e)) - texte_nettoye = texte - - # Remplacer les références aux images - texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye) - texte_nettoye = re.sub(r']+>', '[Image]', texte_nettoye, flags=re.IGNORECASE) - - # Supprimer les éléments courants non pertinents - patterns_a_supprimer = [ - r'Cordialement,[\s\S]*?$', - r'Bien cordialement,[\s\S]*?$', - r'Bonne réception[\s\S]*?$', - r'À votre disposition[\s\S]*?$', - r'Support technique[\s\S]*?$', - r'L\'objectif du Support Technique[\s\S]*?$', - r'Notre service est ouvert[\s\S]*?$', - r'Dès réception[\s\S]*?$', - r'Confidentialité[\s\S]*?$', - r'Ce message électronique[\s\S]*?$', - r'Droit à la déconnexion[\s\S]*?$', - r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$', - r'tél\s*:\s*[\d\s\+]+', - r'mobile\s*:\s*[\d\s\+]+', - r'www\.[^\s]+\.[a-z]{2,3}', - r'\*{10,}.*?\*{10,}', # Lignes de séparation avec astérisques - r'----.*?----', # Lignes de séparation avec tirets - ] - - for pattern in patterns_a_supprimer: - texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE) - - # Supprimer les lignes multiples vides - texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye) - - # Supprimer les espaces multiples - texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye) - - # Convertir les entités HTML - html_entities = { - ' ': ' ', '<': '<', '>': '>', '&': '&', - '"': '"', ''': "'", '€': '€', '©': '©', - '®': '®', 'é': 'é', 'è': 'è', 'à': 'à', - 'ç': 'ç', 'ê': 'ê', 'â': 'â', 'î': 'î', - 'ô': 'ô', 'û': 'û' - } - - for entity, char in html_entities.items(): - texte_nettoye = texte_nettoye.replace(entity, char) - - # Normaliser les caractères accentués - try: - texte_nettoye = normaliser_accents(texte_nettoye) - except Exception as e: - self.ajouter_historique("erreur_normalisation_accents", "Échec", str(e)) - - return texte_nettoye.strip() - - def _detecter_role(self, message: Dict[str, Any]) -> str: - """ - Détecte si un message provient du client ou du support. - - Args: - message: Dictionnaire contenant les informations du message - - Returns: - "Client" ou "Support" - """ - # Vérifier le champ 'role' s'il existe déjà - if "role" in message and message["role"] in ["Client", "Support"]: - return message["role"] - - # Indices de support dans l'email - domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"] - indices_nom_support = ["support", "cbao", "technique", "odoo"] - - email = message.get("email_from", "").lower() - # Nettoyer le format "Nom " - if "<" in email and ">" in email: - match = re.search(r'<([^>]+)>', email) - if match: - email = match.group(1).lower() - - # Vérifier le domaine email - if any(domaine in email for domaine in domaines_support): - return "Support" - - # Vérifier le nom d'auteur - auteur = "" - if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1: - auteur = str(message["author_id"][1]).lower() - elif "auteur" in message: - auteur = str(message["auteur"]).lower() - - if any(indice in auteur for indice in indices_nom_support): - return "Support" - - # Par défaut, considérer comme client - return "Client" - - def _analyser_messages_llm(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - Utilise le LLM pour analyser les messages et en extraire les questions/réponses. - - Args: - messages: Liste des messages à analyser - - Returns: - Analyse des messages et paires de questions/réponses - """ - self.ajouter_historique("analyse_messages_llm", f"{len(messages)} messages", "Analyse en cours...") - - # Vérifier s'il y a des messages à analyser - if len(messages) == 0: - self.ajouter_historique("analyse_messages_llm_erreur", "Aucun message", "La liste des messages est vide") - return { - "success": False, - "error": "Aucun message à analyser", - "messages_analyses": [], - "paires_qr": [] - } - - # Vérifier si nous n'avons qu'un seul message (probablement le message du système) - if len(messages) == 1: - message_unique = messages[0] - role = message_unique.get("role", "") - - # Si c'est un message système, nous n'avons pas de vraie conversation - if role == "system": - self.ajouter_historique("analyse_messages_llm_erreur", "Un seul message système", - "Pas de conversation à analyser") - return { - "success": True, - "messages_analyses": [], - "paires_qr": [] - } - - try: - # Préparation des messages pour le LLM - messages_for_llm = [] - for i, msg in enumerate(messages): - # Inclure uniquement les messages de type Client ou Support - role = msg.get("role", "") - if role not in ["Client", "Support"]: - continue - - # Formater le message pour le LLM - messages_for_llm.append({ - "numero": i + 1, - "role": role, - "date": msg.get("date", ""), - "contenu": msg.get("body", "") - }) - - # S'il n'y a aucun message Client ou Support - if not messages_for_llm: - self.ajouter_historique("analyse_messages_llm_erreur", "Aucun message pertinent", - "Pas de message Client ou Support à analyser") - return { - "success": True, - "messages_analyses": [], - "paires_qr": [] - } - - # Utiliser la nouvelle méthode analyze_messages_json de Mistral - resultat = self.llm.analyze_messages_json(messages_for_llm) - - if "error" in resultat: - self.ajouter_historique("analyse_messages_llm_erreur", "Erreur API", resultat["error"]) - return { - "success": False, - "error": resultat["error"], - "messages_analyses": [], - "paires_qr": [] - } - - contenu = resultat.get("content", "") - self.ajouter_historique("analyse_messages_llm_resultat", "Analyse complétée", contenu[:200] + "...") - - # Traiter la réponse pour extraire les messages analysés - messages_analyses = [] - pattern_messages = r"MESSAGE (\d+):\s*- Rôle: (Client|Support)\s*- Type: (Question|Réponse|Information)\s*- Contenu essentiel: (.*?)(?=MESSAGE \d+:|PAIRE \d+:|$)" - for match in re.finditer(pattern_messages, contenu, re.DOTALL): - num = int(match.group(1)) - role = match.group(2) - type_msg = match.group(3) - contenu_essentiel = match.group(4).strip() - - # Trouver le message correspondant - msg_idx = num - 1 - msg_id = "" - msg_date = "" - - if 0 <= msg_idx < len(messages_for_llm): - original_idx = messages_for_llm[msg_idx]["numero"] - 1 - if 0 <= original_idx < len(messages): - msg_id = messages[original_idx].get("id", "") or messages[original_idx].get("ID", "") - msg_date = messages[original_idx].get("date", "") - - messages_analyses.append({ - "id": msg_id, - "date": msg_date, - "role": role, - "type": type_msg, - "contenu": contenu_essentiel - }) - - # Extraire les paires QR - paires_qr = [] - pattern_paires = r"PAIRE (\d+):\s*- Question \((Client|Support)\): (.*?)(?:\s*- Réponse \((Client|Support)\): (.*?))?(?=PAIRE \d+:|$)" - for match in re.finditer(pattern_paires, contenu, re.DOTALL): - num = match.group(1) - q_role = match.group(2) - question = match.group(3).strip() - r_role = match.group(4) if match.group(4) else "" - reponse = match.group(5).strip() if match.group(5) else "" - - paires_qr.append({ - "numero": num, - "question": { - "role": q_role, - "contenu": question - }, - "reponse": { - "role": r_role, - "contenu": reponse - } if reponse else None - }) - - # Ne pas générer de paires artificielles si le LLM n'en a pas détecté - if not paires_qr: - self.ajouter_historique("analyse_paires_qr", "Aucune paire", "Le LLM n'a détecté aucune paire question/réponse") - - return { - "success": True, - "messages_analyses": messages_analyses, - "paires_qr": paires_qr - } - - except Exception as e: - self.ajouter_historique("analyse_messages_llm_erreur", f"{len(messages)} messages", str(e)) - return { - "success": False, - "error": str(e), - "messages_analyses": [], - "paires_qr": [] - } - - def _generer_tableau_markdown(self, paires_qr: List[Dict[str, Any]]) -> str: - """ - Génère un tableau Markdown avec les questions et réponses. - - Args: - paires_qr: Liste de paires question/réponse - - Returns: - Tableau Markdown formaté - """ - # Créer le tableau - markdown = ["# Analyse des Questions et Réponses\n"] - markdown.append("| Question | Réponse |") - markdown.append("|---------|---------|") - - for paire in paires_qr: - question = paire.get("question", {}) - reponse = paire.get("reponse", {}) - - q_role = question.get("role", "Client") - q_contenu = question.get("contenu", "") - - # Normaliser le contenu des questions pour corriger les accents - q_contenu = normaliser_accents(q_contenu) - - if reponse: - r_role = reponse.get("role", "Support") - r_contenu = reponse.get("contenu", "") - - # Normaliser le contenu des réponses pour corriger les accents - r_contenu = normaliser_accents(r_contenu) - - markdown.append(f"| **{q_role}**: {q_contenu} | **{r_role}**: {r_contenu} |") - else: - markdown.append(f"| **{q_role}**: {q_contenu} | *Pas de réponse* |") - - # Ajouter les informations sur les paramètres LLM utilisés - markdown.append("\n## Paramètres LLM utilisés\n") - params = self.generer_rapport_parametres() - markdown.append(f"- **Type de LLM**: {params['llm_type']}") - markdown.append(f"- **Modèle**: {params['parametres'].get('model', 'Non spécifié')}") - markdown.append(f"- **Température**: {params['parametres'].get('temperature', 'Non spécifiée')}") - markdown.append(f"- **Tokens max**: {params['parametres'].get('max_tokens', 'Non spécifié')}") - - if params['parametres_modifies']: - markdown.append("\n**Paramètres modifiés durant l'analyse:**") - for param, valeur in params['parametres_modifies'].items(): - if param != 'system_prompt': # Exclure le system_prompt car trop long - markdown.append(f"- **{param}**: {valeur}") - - # Normaliser tout le contenu markdown final pour s'assurer que tous les accents sont corrects - return normaliser_accents("\n".join(markdown)) - - def executer(self, messages_data: List[Dict[str, Any]], output_path: Optional[str] = None) -> Dict[str, Any]: - """ - Analyse les messages pour extraire les questions et réponses. - - Args: - messages_data: Liste des messages du ticket - output_path: Chemin où sauvegarder le tableau Markdown (optionnel) - - Returns: - Résultats de l'analyse avec le tableau Markdown - """ - self.ajouter_historique("debut_execution", f"{len(messages_data)} messages", "Début de l'analyse") - - try: - # Préparation des messages - messages_prepares = [] - for msg in messages_data: - # Nettoyer le contenu - contenu = msg.get("body", "") or msg.get("contenu", "") - contenu_nettoye = self._nettoyer_contenu(contenu) - - # Détecter le rôle - role = self._detecter_role(msg) - - # Ajouter le message préparé si non vide après nettoyage - if contenu_nettoye.strip(): - messages_prepares.append({ - "id": msg.get("id", "") or msg.get("ID", ""), - "date": msg.get("date", ""), - "author_id": msg.get("author_id", []), - "email_from": msg.get("email_from", ""), - "role": role, - "body": contenu_nettoye - }) - - # Trier par date si disponible - messages_prepares.sort(key=lambda x: x.get("date", "")) - - # Analyser avec le LLM - resultats_analyse = self._analyser_messages_llm(messages_prepares) - - # Générer le tableau Markdown avec normalisation des accents - tableau_md = self._generer_tableau_markdown(resultats_analyse.get("paires_qr", [])) - - # Dernière vérification pour s'assurer que les accents sont normalisés - tableau_md = normaliser_accents(tableau_md) - - # Sauvegarder le tableau si un chemin est fourni - if output_path: - try: - # Créer le dossier parent si nécessaire - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - with open(output_path, 'w', encoding='utf-8') as f: - f.write(tableau_md) - - self.ajouter_historique("sauvegarde_tableau", output_path, "Tableau sauvegardé") - except Exception as e: - self.ajouter_historique("erreur_sauvegarde", output_path, str(e)) - - # Préparer le résultat - resultat = { - "success": resultats_analyse.get("success", False), - "messages_analyses": resultats_analyse.get("messages_analyses", []), - "paires_qr": resultats_analyse.get("paires_qr", []), - "nb_questions": len(resultats_analyse.get("paires_qr", [])), - "nb_reponses": sum(1 for p in resultats_analyse.get("paires_qr", []) if p.get("reponse")), - "tableau_md": tableau_md, - "parametres_llm": self.generer_rapport_parametres() - } - - self.ajouter_historique("fin_execution", "Analyse terminée", - f"{resultat['nb_questions']} questions, {resultat['nb_reponses']} réponses") - - return resultat - - except Exception as e: - erreur = f"Erreur lors de l'analyse: {str(e)}" - self.ajouter_historique("erreur_execution", "Exception", erreur) - return { - "success": False, - "error": erreur - } \ No newline at end of file diff --git a/config.json b/config.json deleted file mode 100644 index f65b526..0000000 --- a/config.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "odoo": { - "url": "https://odoo.cbao.fr", - "db": "production_cbao", - "username": "fernand@cbao.fr", - "api_key": "Lestat66!" - }, - "llm": { - "api_key": "your_mistral_api_key" - }, - "output_dir": "output" -} \ No newline at end of file diff --git a/config.json.example b/config.json.example deleted file mode 100644 index 0baee2f..0000000 --- a/config.json.example +++ /dev/null @@ -1,12 +0,0 @@ -{ - "odoo": { - "url": "https://example.odoo.com", - "db": "database_name", - "username": "user@example.com", - "api_key": "your_odoo_api_key_or_password" - }, - "llm": { - "api_key": "your_mistral_api_key" - }, - "output_dir": "output" -} \ No newline at end of file diff --git a/extract_ticket.log b/extract_ticket.log deleted file mode 100644 index bc76c84..0000000 --- a/extract_ticket.log +++ /dev/null @@ -1,4 +0,0 @@ -2025-04-02 11:39:56,293 - extract_ticket - INFO - Prétraitement du ticket: output/ticket_T0167 -> output_processed/ticket_T0167 -2025-04-02 11:39:56,296 - extract_ticket - INFO - Ticket info prétraité et sauvegardé: output_processed/ticket_T0167/ticket_info.json -2025-04-02 11:39:56,297 - extract_ticket - INFO - Messages prétraités et sauvegardés: output_processed/ticket_T0167/messages.json (2 messages) -2025-04-02 11:39:56,297 - extract_ticket - INFO - Rapport de prétraitement sauvegardé: output_processed/ticket_T0167/pretraitement_rapport.json diff --git a/filter_images.log b/filter_images.log deleted file mode 100644 index 5cba154..0000000 --- a/filter_images.log +++ /dev/null @@ -1 +0,0 @@ -2025-04-02 11:41:31,283 - filter_images - ERROR - Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH. diff --git a/llm-ticket3.code-workspace b/llm-ticket3.code-workspace deleted file mode 100644 index 38d52e0..0000000 --- a/llm-ticket3.code-workspace +++ /dev/null @@ -1,14 +0,0 @@ -{ - "folders": [ - { - "path": "." - }, - { - "path": "../odoo_toolkit" - }, - { - "path": "../llm-ticket2" - } - ], - "settings": {} -} \ No newline at end of file diff --git a/llm/__init__.py b/llm/__init__.py deleted file mode 100644 index 2d072ef..0000000 --- a/llm/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from .llm_base import LLM -from .mistral import Mistral -from .pixtral import Pixtral - -__all__ = ['LLM', 'Mistral', 'Pixtral'] \ No newline at end of file diff --git a/llm/__pycache__/__init__.cpython-312.pyc b/llm/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d04d5bd..0000000 Binary files a/llm/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/llm/__pycache__/llm_base.cpython-312.pyc b/llm/__pycache__/llm_base.cpython-312.pyc deleted file mode 100644 index cfc5424..0000000 Binary files a/llm/__pycache__/llm_base.cpython-312.pyc and /dev/null differ diff --git a/llm/__pycache__/mistral.cpython-312.pyc b/llm/__pycache__/mistral.cpython-312.pyc deleted file mode 100644 index 87b3e7f..0000000 Binary files a/llm/__pycache__/mistral.cpython-312.pyc and /dev/null differ diff --git a/llm/__pycache__/pixtral.cpython-312.pyc b/llm/__pycache__/pixtral.cpython-312.pyc deleted file mode 100644 index 559f1b2..0000000 Binary files a/llm/__pycache__/pixtral.cpython-312.pyc and /dev/null differ diff --git a/llm/llm_base.py b/llm/llm_base.py deleted file mode 100644 index 5ba5740..0000000 --- a/llm/llm_base.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Module définissant la classe abstraite de base pour les modèles LLM. -""" - -from abc import ABC, abstractmethod -from typing import Dict, Any, Optional - -class LLM(ABC): - """ - Classe abstraite définissant l'interface commune pour tous les modèles LLM. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'instance LLM avec une clé API. - - Args: - api_key: Clé API pour accéder au service LLM - """ - self.api_key = api_key - self.system_prompt = "" - - @abstractmethod - def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]: - """ - Génère une réponse à partir d'un prompt. - - Args: - prompt: Texte d'entrée pour la génération - **kwargs: Options supplémentaires - - Returns: - Dictionnaire contenant la réponse et les métadonnées - """ - pass - - @abstractmethod - def analyze_image(self, image_path: str, prompt: str, **kwargs) -> Dict[str, Any]: - """ - Analyse une image selon un prompt. - - Args: - image_path: Chemin vers l'image à analyser - prompt: Instructions pour l'analyse - **kwargs: Options supplémentaires - - Returns: - Dictionnaire contenant l'analyse et les métadonnées - """ - pass \ No newline at end of file diff --git a/llm/mistral.py b/llm/mistral.py deleted file mode 100644 index 8c1b130..0000000 --- a/llm/mistral.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Module implémentant la classe Mistral pour interagir avec l'API Mistral AI. -""" - -import os -import requests -import json -from typing import Dict, List, Any, Optional - -from .llm_base import LLM - -class Mistral(LLM): - """ - Implémentation de l'interface LLM pour Mistral AI. - """ - API_URL = "https://api.mistral.ai/v1/chat/completions" - DEFAULT_MODEL = "mistral-medium" - - def __init__(self, api_key: Optional[str] = None): - """ - Initialise le client Mistral. - - Args: - api_key: Clé API Mistral (si None, utilise la variable d'environnement MISTRAL_API_KEY) - """ - api_key = api_key or os.environ.get("MISTRAL_API_KEY") - super().__init__(api_key) - - # Configuration par défaut - self.model = self.DEFAULT_MODEL - self.temperature = 0.7 - self.max_tokens = 2000 - self.top_p = 1.0 - - # Prompt système par défaut - self.system_prompt = ( - "Vous êtes un expert en support technique pour les logiciels spécialisés. " - "Analysez précisément les problèmes techniques décrits." - ) - - # État d'initialisation - self.initialized = True - self.headers = None - - def initialize(self) -> None: - """ - Initialise la connexion à l'API. - """ - if not self.api_key: - print("Mode simulation: Aucune clé API nécessaire.") - - self.headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}" - } - - self.initialized = True - - def validate_and_parse_json(self, messages_data: Any) -> List[Dict[str, Any]]: - """ - Valide et analyse les données JSON. - - Args: - messages_data: Données JSON en string ou déjà décodées - - Returns: - Liste d'objets messages validés - """ - if isinstance(messages_data, str): - try: - messages = json.loads(messages_data) - except json.JSONDecodeError: - return [{"error": "Format JSON invalide", "content": messages_data}] - else: - messages = messages_data - - if not isinstance(messages, list): - return [{"error": "Le format attendu est une liste de messages", "content": str(messages)}] - - return messages - - def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]: - """ - Génère une réponse textuelle à partir d'un prompt. - - Args: - prompt: Texte d'entrée pour la génération - **kwargs: Options supplémentaires - - Returns: - Dictionnaire contenant la réponse et les métadonnées - """ - print("Mode simulation: Génération de réponse textuelle") - - # Simulation d'une réponse - response = { - "content": f"Je suis un modèle simulé. Voici ma réponse à votre prompt: {prompt[:100]}...", - "model": self.model, - "usage": { - "prompt_tokens": len(prompt) // 4, - "completion_tokens": 200, - "total_tokens": len(prompt) // 4 + 200 - } - } - - return response - - def analyze_messages_json(self, messages_json: Any, **kwargs) -> Dict[str, Any]: - """ - Analyse les messages fournis au format JSON pour extraire les questions et réponses. - - Args: - messages_json: Messages au format JSON (liste d'objets ou chaîne JSON) - **kwargs: Options supplémentaires - - Returns: - Analyse des messages avec identification des questions et réponses - """ - print("Mode simulation: Analyse de messages JSON") - - # Valider et analyser le JSON - messages = self.validate_and_parse_json(messages_json) - - if any(msg.get("error") for msg in messages): - error_msg = next((msg.get("error") for msg in messages if msg.get("error")), "Erreur de format JSON") - return {"error": error_msg, "content": ""} - - # Extraire les informations du ticket et de contexte - ticket_info = next((msg for msg in messages if msg.get("id") == "ticket_info"), {}) - ticket_code = ticket_info.get("code", "Inconnu") - ticket_name = ticket_info.get("name", "Ticket sans titre") - ticket_desc = ticket_info.get("description", "") - - # Séparer les messages par rôle - context_msgs = [msg for msg in messages if msg.get("role") == "system" or msg.get("type") == "contexte"] - client_msgs = [msg for msg in messages if msg.get("role") == "Client"] - support_msgs = [msg for msg in messages if msg.get("role") == "Support"] - other_msgs = [msg for msg in messages if msg.get("role") not in ["system", "Client", "Support"] and msg.get("type") != "contexte"] - - # Organisation des messages par ordre chronologique pour analyse - all_content_msgs = client_msgs + support_msgs + other_msgs - # Trier par date si possible - sorted_msgs = sorted(all_content_msgs, key=lambda x: x.get("date", "0"), reverse=False) - - # Préparer l'analyse des messages - message_analyses = [] - for i, msg in enumerate(sorted_msgs): - role = msg.get("role", "Inconnu") - msg_type = msg.get("type", "Information" if role == "Support" else "Question") - body = msg.get("body", "").strip() - - if body: - message_analyses.append({ - "numero": i + 1, - "role": role, - "type": msg_type, - "contenu": body[:500] # Limiter la longueur du contenu - }) - - # Extraire les paires question-réponse - pairs_qr = [] - current_question = None - - for msg in sorted_msgs: - role = msg.get("role", "Inconnu") - body = msg.get("body", "").strip() - - if not body: - continue - - if role == "Client" or (role not in ["Support", "system"] and not current_question): - # Nouveau client message = nouvelle question potentielle - current_question = { - "role": role, - "contenu": body - } - elif role == "Support" and current_question: - # Message de support après une question = réponse potentielle - pairs_qr.append({ - "numero": len(pairs_qr) + 1, - "question": current_question, - "reponse": { - "role": role, - "contenu": body - } - }) - current_question = None - - # Ajouter les questions sans réponse - if current_question: - pairs_qr.append({ - "numero": len(pairs_qr) + 1, - "question": current_question, - "reponse": None - }) - - # Générer le résultat formaté - result = f"ANALYSE DU TICKET {ticket_code}: {ticket_name}\n\n" - - # Ajouter les analyses de messages - for i, msg in enumerate(message_analyses): - result += f"MESSAGE {msg['numero']}:\n" - result += f"- Rôle: {msg['role']}\n" - result += f"- Type: {msg['type']}\n" - result += f"- Contenu essentiel: {msg['contenu']}\n\n" - - # Ajouter les paires question-réponse - for pair in pairs_qr: - result += f"PAIRE {pair['numero']}:\n" - result += f"- Question ({pair['question']['role']}): {pair['question']['contenu']}\n" - if pair['reponse']: - result += f"- Réponse ({pair['reponse']['role']}): {pair['reponse']['contenu']}\n\n" - else: - result += "- Réponse: Aucune réponse trouvée\n\n" - - return { - "content": result, - "model": self.model, - "usage": { - "prompt_tokens": sum(len(msg.get("body", "")) // 4 for msg in messages), - "completion_tokens": len(result) // 2, - "total_tokens": sum(len(msg.get("body", "")) // 4 for msg in messages) + len(result) // 2 - } - } - - def analyze_image(self, image_path: str, prompt: str, **kwargs) -> Dict[str, Any]: - """ - Analyse une image (non supporté par Mistral standard). - - Args: - image_path: Chemin vers l'image à analyser - prompt: Instructions pour l'analyse - **kwargs: Options supplémentaires - - Returns: - Dictionnaire d'erreur (fonctionnalité non supportée) - """ - return { - "error": "L'analyse d'images n'est pas supportée par Mistral. Utilisez Pixtral." - } \ No newline at end of file diff --git a/llm/pixtral.py b/llm/pixtral.py deleted file mode 100644 index 42b75e9..0000000 --- a/llm/pixtral.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Module implémentant la classe Pixtral pour l'analyse d'images via l'API Mistral AI. -""" - -import os -import base64 -import requests -import json -from typing import Dict, Any, Optional, List - -from .llm_base import LLM - -class Pixtral(LLM): - """ - Implémentation de l'interface LLM pour Pixtral (Mistral AI avec vision). - """ - API_URL = "https://api.mistral.ai/v1/chat/completions" - DEFAULT_MODEL = "pixtral-12b-2409" - - def __init__(self, api_key: Optional[str] = None): - """ - Initialise le client Pixtral. - - Args: - api_key: Clé API Mistral (si None, utilise la variable d'environnement MISTRAL_API_KEY) - """ - api_key = api_key or os.environ.get("MISTRAL_API_KEY") - super().__init__(api_key) - - # Configuration par défaut - self.model = self.DEFAULT_MODEL - self.temperature = 0.3 - self.max_tokens = 1024 - self.top_p = 1.0 - - # Prompt système par défaut - self.system_prompt = ( - "Vous êtes un expert en analyse d'images techniques. " - "Décrivez précisément ce que vous voyez et identifiez les problèmes visibles." - ) - - # État d'initialisation - self.initialized = True - self.headers = None - - def initialize(self) -> None: - """ - Initialise la connexion à l'API. - """ - if not self.api_key: - print("Mode simulation: Aucune clé API nécessaire.") - - self.headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}" - } - - self.initialized = True - - def _encode_image(self, image_path: str) -> str: - """ - Encode une image en base64 pour l'API. - - Args: - image_path: Chemin vers l'image - - Returns: - Image encodée en base64 avec le bon format URI - """ - if not os.path.isfile(image_path): - raise FileNotFoundError(f"Image non trouvée: {image_path}") - - try: - with open(image_path, "rb") as image_file: - encoded_string = base64.b64encode(image_file.read()).decode("utf-8") - - # Déterminer le type MIME - file_extension = os.path.splitext(image_path)[1].lower() - if file_extension in ['.jpg', '.jpeg']: - mime_type = 'image/jpeg' - elif file_extension == '.png': - mime_type = 'image/png' - elif file_extension == '.gif': - mime_type = 'image/gif' - elif file_extension == '.webp': - mime_type = 'image/webp' - else: - mime_type = 'image/jpeg' # Par défaut - - return f"data:{mime_type};base64,{encoded_string}" - except Exception as e: - raise IOError(f"Erreur lors de l'encodage de l'image {image_path}: {str(e)}") - - def generate_response(self, prompt: str, **kwargs) -> Dict[str, Any]: - """ - Génère une réponse textuelle à partir d'un prompt. - - Args: - prompt: Texte d'entrée pour la génération - **kwargs: Options supplémentaires - - Returns: - Dictionnaire contenant la réponse et les métadonnées - """ - print("Mode simulation: Génération de réponse textuelle") - - # Simulation d'une réponse - response = { - "content": f"Je suis un modèle simulé. Voici ma réponse à votre prompt: {prompt[:50]}...", - "model": self.model, - "usage": { - "prompt_tokens": len(prompt) // 4, - "completion_tokens": 100, - "total_tokens": len(prompt) // 4 + 100 - } - } - - return response - - def analyze_image(self, image_path: str, prompt: str, contexte: str = "", **kwargs) -> Dict[str, Any]: - """ - Analyse une image selon un prompt et un contexte optionnel. - - Args: - image_path: Chemin vers l'image à analyser - prompt: Instructions pour l'analyse - contexte: Contexte du ticket pour contextualiser l'analyse (optionnel) - **kwargs: Options supplémentaires - - Returns: - Dictionnaire contenant l'analyse et les métadonnées - """ - if not os.path.exists(image_path): - return { - "error": f"Image introuvable: {image_path}", - "content": "", - "model": self.model - } - - print(f"Mode simulation: Analyse d'image {os.path.basename(image_path)}") - - # Vérifier le type de fichier - try: - file_extension = os.path.splitext(image_path)[1].lower() - file_size = os.path.getsize(image_path) - if file_extension not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: - return { - "error": f"Format d'image non supporté: {file_extension}", - "content": "", - "model": self.model - } - - if file_size > 10 * 1024 * 1024: # 10 MB - return { - "error": f"Image trop volumineuse ({file_size/1024/1024:.2f} MB), max 10 MB", - "content": "", - "model": self.model - } - except Exception as e: - return { - "error": f"Erreur lors de la vérification de l'image: {str(e)}", - "content": "", - "model": self.model - } - - # Analyser le nom du fichier et le contexte pour la simulation - filename = os.path.basename(image_path).lower() - is_logo = any(substr in filename for substr in ["logo", "signature", "icon", "avatar", "image003"]) - is_screenshot = any(substr in filename for substr in ["screen", "capture", "scr", "interface", "error", "bug", "image004", "image"]) - is_diagram = any(substr in filename for substr in ["diagram", "flow", "schema", "archi"]) - - # Si le contexte spécifie des problèmes techniques, favoriser la pertinence des captures d'écran - ticket_has_technical_issue = False - if contexte: - technical_keywords = ["problème", "erreur", "bug", "dysfonctionnement", "ne fonctionne pas", "modification"] - ticket_has_technical_issue = any(kw in contexte.lower() for kw in technical_keywords) - - # Simuler une réponse d'analyse d'image - if is_logo or (not is_screenshot and not is_diagram): - # Simuler une image non pertinente - content = json.dumps({ - "pertinente": False, - "type_image": "logo" if is_logo else "autre", - "description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.", - "confiance": 90, - "justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème." - }, indent=2) - else: - # Simuler une image pertinente (capture d'écran ou diagramme) - image_type = "capture_ecran" if is_screenshot else "schema" if is_diagram else "autre" - description = "" - - if is_screenshot: - description = "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage." - if ticket_has_technical_issue and "image004" in filename: - description = "Capture d'écran montrant l'interface de gestion des centrales d'enrobage. On peut voir un formulaire avec le champ de nom et une flèche à droite qui permet de modifier certaines propriétés." - elif is_diagram: - description = "Schéma technique montrant l'architecture ou le flux de données du système." - else: - description = "Image technique liée au contexte du ticket." - - content = json.dumps({ - "pertinente": True, - "type_image": image_type, - "description": description, - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket." - }, indent=2) - - return { - "content": content, - "model": self.model, - "usage": { - "prompt_tokens": len(prompt) // 4 + (len(contexte) // 8 if contexte else 0), - "completion_tokens": 150, - "total_tokens": len(prompt) // 4 + (len(contexte) // 8 if contexte else 0) + 150 - }, - "image_analyzed": os.path.basename(image_path) - } - - def analyze_images_batch(self, image_paths: List[str], prompt: str, contexte: str = "", **kwargs) -> List[Dict[str, Any]]: - """ - Analyse un lot d'images en une seule fois. - - Args: - image_paths: Liste des chemins vers les images à analyser - prompt: Instructions pour l'analyse - contexte: Contexte du ticket (optionnel) - **kwargs: Options supplémentaires - - Returns: - Liste de dictionnaires contenant les analyses - """ - results = [] - for image_path in image_paths: - result = self.analyze_image(image_path, prompt, contexte, **kwargs) - results.append({ - "image_path": image_path, - "result": result - }) - - return results \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index e5eb3fa..0000000 --- a/main.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script principal pour l'analyse de tickets de support. -Ce script coordonne l'extraction de données depuis Odoo et l'analyse avec les agents LLM. -""" - -import os -import json -import argparse -import subprocess -import shutil -import re -from typing import Dict, List, Any, Optional - -from utils import TicketAnalyzer, TicketManager -from post_process import transformer_messages, corriger_json_accents, corriger_markdown_accents, reparer_ticket - -def charger_config(config_path: str) -> Dict[str, Any]: - """ - Charge la configuration depuis un fichier JSON. - - Args: - config_path: Chemin vers le fichier de configuration - - Returns: - Dictionnaire de configuration - """ - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as e: - print(f"Erreur lors du chargement de la configuration: {e}") - # Configuration par défaut minimale - return { - "odoo": { - "url": "https://example.odoo.com", - "db": "db_name", - "username": "user@example.com", - "api_key": "your_api_key" - }, - "llm": { - "api_key": "your_mistral_api_key" - }, - "output_dir": "output" - } - -def extraire_ticket(config: Dict[str, Any], ticket_code: str, output_dir: str) -> Dict[str, Any]: - """ - Extrait les données d'un ticket depuis Odoo. - - Args: - config: Configuration avec les paramètres de connexion - ticket_code: Code du ticket à extraire - output_dir: Répertoire où sauvegarder les données - - Returns: - Données du ticket extraites - """ - # Créer le gestionnaire de tickets - manager = TicketManager( - url=config["odoo"]["url"], - db=config["odoo"]["db"], - username=config["odoo"]["username"], - api_key=config["odoo"]["api_key"] - ) - - # Récupérer le ticket par son code - ticket = manager.get_ticket_by_code(ticket_code) - if not ticket: - print(f"Ticket {ticket_code} non trouvé") - return {} - - # Extraire toutes les données du ticket - ticket_dir = os.path.join(output_dir, f"ticket_{ticket_code}") - ticket_data = manager.extract_ticket_data(ticket["id"], ticket_dir) - - # Post-traiter immédiatement les messages - post_traiter_messages(ticket_dir) - - return ticket_data - -def post_traiter_messages(ticket_dir: str) -> None: - """ - Post-traite les messages du ticket pour une meilleure analyse. - - Args: - ticket_dir: Répertoire contenant les données du ticket - """ - messages_file = os.path.join(ticket_dir, "messages.json") - - # Vérifier que le fichier existe - if not os.path.exists(messages_file): - print(f"AVERTISSEMENT: Fichier messages.json introuvable dans {ticket_dir}") - return - - print(f"Post-traitement des messages du ticket...") - - # Créer une sauvegarde avant transformation - backup_file = os.path.join(ticket_dir, "messages.json.backup") - if not os.path.exists(backup_file): - shutil.copy2(messages_file, backup_file) - print(f"Sauvegarde créée: {backup_file}") - - # Transformer les messages pour un format optimal - transformer_messages(messages_file) - - # Vérifier que la transformation a réussi - try: - with open(messages_file, 'r', encoding='utf-8') as f: - messages = json.load(f) - print(f"Post-traitement terminé, {len(messages)} messages formatés.") - except Exception as e: - print(f"ERREUR: Échec du post-traitement: {e}") - # Restaurer la sauvegarde si nécessaire - if os.path.exists(backup_file): - shutil.copy2(backup_file, messages_file) - print("Restauration de la sauvegarde des messages.") - -def preparer_donnees_ticket(ticket_dir: str) -> Dict[str, Any]: - """ - Prépare les données du ticket pour l'analyse à partir des fichiers stockés. - - Args: - ticket_dir: Répertoire contenant les données du ticket - - Returns: - Dictionnaire des données du ticket prêtes pour l'analyse - """ - # Chemins des fichiers sources - ticket_file = os.path.join(ticket_dir, "ticket_info.json") - messages_file = os.path.join(ticket_dir, "messages.json") - attachments_file = os.path.join(ticket_dir, "attachments_info.json") - attachments_dir = os.path.join(ticket_dir, "attachments") - - # Vérifier que les fichiers nécessaires existent - if not all(os.path.exists(f) for f in [ticket_file, messages_file, attachments_file]): - missing = [f for f in [ticket_file, messages_file, attachments_file] if not os.path.exists(f)] - raise FileNotFoundError(f"Fichiers manquants: {', '.join(missing)}") - - # Charger les données - try: - with open(ticket_file, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - with open(messages_file, 'r', encoding='utf-8') as f: - messages = json.load(f) - - with open(attachments_file, 'r', encoding='utf-8') as f: - attachments = json.load(f) - - # Vérifier et corriger les chemins des pièces jointes - for attachment in attachments: - if "file_path" in attachment: - # S'assurer que le chemin est absolu - if not os.path.isabs(attachment["file_path"]): - attachment["file_path"] = os.path.join(attachments_dir, os.path.basename(attachment["file_path"])) - - # Vérifier que le fichier existe - if not os.path.exists(attachment["file_path"]): - print(f"AVERTISSEMENT: Pièce jointe introuvable: {attachment['file_path']}") - - return { - "ticket": ticket_info, - "messages": messages, - "attachments": attachments, - "files": { - "ticket_info": ticket_file, - "messages": messages_file, - "attachments_info": attachments_file, - "attachments_dir": attachments_dir - } - } - - except Exception as e: - raise ValueError(f"Erreur lors du chargement des données du ticket: {e}") - -def analyser_ticket(ticket_data: Dict[str, Any], config: Dict[str, Any], output_dir: str, llm_params: Optional[Dict[str, Any]] = None) -> Dict[str, str]: - """ - Analyse un ticket avec les agents LLM. - - Args: - ticket_data: Données du ticket extraites - config: Configuration avec les clés API - output_dir: Répertoire où sauvegarder les résultats - llm_params: Paramètres LLM globaux à appliquer - - Returns: - Chemins des fichiers générés - """ - # Créer l'analyseur de tickets - analyzer = TicketAnalyzer(api_key=config["llm"]["api_key"], llm_params=llm_params) - - # Préparer le contexte pour l'analyse des images - ticket_info = ticket_data.get("ticket", {}) - contexte = f""" - TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')} - - DESCRIPTION: - {ticket_info.get('description', 'Aucune description')} - """ - - # Récupérer les chemins des pièces jointes (images) - attachments = ticket_data.get("attachments", []) - image_paths = [] - - # Vérification des doublons par nom de fichier - image_noms = set() - - for attachment in attachments: - chemin = attachment.get("file_path") - nom_fichier = os.path.basename(chemin) if chemin else "" - - if not chemin or not os.path.exists(chemin): - continue - - mimetype = attachment.get("mimetype", "") - - # Vérifier que c'est une image et qu'on ne l'a pas déjà incluse - if mimetype.startswith("image/") and nom_fichier not in image_noms: - image_paths.append(chemin) - image_noms.add(nom_fichier) - print(f"Image ajoutée pour analyse: {nom_fichier}") - - # Filtrer les images pertinentes - print(f"Filtrage de {len(image_paths)} images...") - images_pertinentes = analyzer.filtrer_images(image_paths) - print(f"Images pertinentes: {len(images_pertinentes)}/{len(image_paths)}") - - # Imprimer les détails pour le débogage - for i, img in enumerate(image_paths): - est_pertinente = img in images_pertinentes - print(f" Image {i+1}: {os.path.basename(img)} - {'Pertinente' if est_pertinente else 'Non pertinente'}") - - # Analyser les images pertinentes - print("Analyse des images pertinentes...") - resultats_images = analyzer.analyser_images(images_pertinentes, contexte) - print(f"Analyses d'images terminées: {len(resultats_images)}") - - # Extraire les questions et réponses - print("Extraction des questions et réponses...") - messages = ticket_data.get("messages", []) - - # Vérifier que les messages sont traités et ne contiennent pas de balises HTML - for msg in messages: - body = msg.get("body", "") - if isinstance(body, str) and re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE): - print(f"AVERTISSEMENT: Message {msg.get('id', 'inconnu')} contient du HTML non traité") - - qr_path = os.path.join(output_dir, "questions_reponses.md") - resultats_qr = analyzer.extraire_questions_reponses(messages, qr_path) - print(f"Questions extraites: {resultats_qr.get('nb_questions', 0)}") - print(f"Réponses extraites: {resultats_qr.get('nb_reponses', 0)}") - - # Générer le rapport final - print("Génération du rapport final...") - rapport_dir = os.path.join(output_dir, "rapport") - fichiers = analyzer.generer_rapport(rapport_dir) - print(f"Rapport généré: {rapport_dir}") - - # Corriger les problèmes d'accents dans les fichiers JSON - corriger_accents_fichiers(rapport_dir) - - return fichiers - -def corriger_accents_fichiers(dir_path: str) -> None: - """ - Corrige les problèmes d'accents dans tous les fichiers JSON et Markdown d'un répertoire. - - Args: - dir_path: Chemin du répertoire contenant les fichiers à corriger - """ - print("Correction des problèmes d'accents dans les fichiers...") - - if not os.path.exists(dir_path): - print(f"Répertoire non trouvé: {dir_path}") - return - - for root, _, files in os.walk(dir_path): - for file in files: - # Corriger les fichiers JSON - if file.endswith(".json"): - json_file = os.path.join(root, file) - corriger_json_accents(json_file) - - # Corriger les fichiers Markdown - elif file.endswith(".md"): - md_file = os.path.join(root, file) - corriger_markdown_accents(md_file) - - # Corriger également les fichiers Markdown du répertoire de ticket - ticket_dir = os.path.dirname(dir_path) - for file in os.listdir(ticket_dir): - if file.endswith(".md"): - md_file = os.path.join(ticket_dir, file) - corriger_markdown_accents(md_file) - - print("Correction des accents terminée.") - -def main(): - """ - Fonction principale du script. - """ - # Parser les arguments de la ligne de commande - parser = argparse.ArgumentParser(description="Analyse de tickets de support") - parser.add_argument("ticket_code", help="Code du ticket à analyser") - parser.add_argument("--config", "-c", default="config.json", help="Chemin vers le fichier de configuration") - parser.add_argument("--output", "-o", default="output", help="Répertoire de sortie") - parser.add_argument("--skip-extraction", "-s", action="store_true", help="Ignorer l'extraction du ticket (utiliser les données existantes)") - parser.add_argument("--fix-accents", "-f", action="store_true", help="Corriger les problèmes d'accents dans les fichiers existants") - parser.add_argument("--llm-params", "-p", type=str, help="Paramètres LLM au format JSON (ex: '{\"temperature\": 0.5}')") - parser.add_argument("--reprocess", "-r", action="store_true", help="Forcer le retraitement des messages même si l'extraction est ignorée") - parser.add_argument("--repair", action="store_true", help="Réparer un ticket corrompu avant analyse") - args = parser.parse_args() - - # Charger la configuration - config = charger_config(args.config) - - # Charger les paramètres LLM supplémentaires si spécifiés - llm_params = {} - if args.llm_params: - try: - llm_params = json.loads(args.llm_params) - print(f"Paramètres LLM personnalisés: {llm_params}") - except json.JSONDecodeError as e: - print(f"Erreur lors du parsing des paramètres LLM: {e}") - - # Créer le répertoire de sortie - os.makedirs(args.output, exist_ok=True) - - # Construire le chemin du répertoire du ticket - ticket_dir = os.path.join(args.output, f"ticket_{args.ticket_code}") - - # Réparer le ticket si demandé - if args.repair: - if os.path.exists(ticket_dir): - print(f"Réparation du ticket {args.ticket_code}...") - success = reparer_ticket(ticket_dir) - if not success: - print("ERREUR: La réparation du ticket a échoué. Impossible de continuer.") - return - print(f"Réparation terminée, poursuite de l'analyse...") - else: - print(f"Impossible de réparer: répertoire du ticket {args.ticket_code} introuvable.") - if not args.skip_extraction: - print("Le ticket sera extrait depuis Odoo.") - else: - print("ERREUR: Impossible de continuer sans extraction.") - return - - # Si l'option de correction des accents est activée uniquement - if args.fix_accents and not args.skip_extraction and not args.repair: - rapport_dir = os.path.join(ticket_dir, "rapport") - if os.path.exists(rapport_dir): - corriger_accents_fichiers(rapport_dir) - return - - # Extraction ou chargement des données du ticket - try: - if not args.skip_extraction: - # Extraire les données du ticket - print(f"Extraction du ticket {args.ticket_code}...") - ticket_data = extraire_ticket(config, args.ticket_code, args.output) - if not ticket_data: - print("Impossible de continuer sans données de ticket.") - return - else: - # Si on ignore l'extraction mais qu'on veut retraiter - if args.reprocess: - print(f"Retraitement forcé des messages du ticket {args.ticket_code}...") - post_traiter_messages(ticket_dir) - - # Charger les données existantes - print(f"Chargement des données du ticket {args.ticket_code}...") - ticket_data = preparer_donnees_ticket(ticket_dir) - print("Données chargées avec succès.") - - # Analyser le ticket - print(f"Analyse du ticket {args.ticket_code}...") - fichiers = analyser_ticket(ticket_data, config, ticket_dir, llm_params) - - print("\nAnalyse terminée!") - print(f"Rapport JSON: {fichiers['json']}") - print(f"Rapport Markdown: {fichiers['markdown']}") - - except FileNotFoundError as e: - print(f"ERREUR: {e}") - print("Utilisez l'extraction ou assurez-vous que tous les fichiers nécessaires existent.") - print("Ou bien utilisez l'option --repair pour réparer le ticket.") - except ValueError as e: - print(f"ERREUR: {e}") - print("Vous pouvez essayer l'option --repair pour réparer le ticket.") - except Exception as e: - print(f"ERREUR inattendue: {e}") - print("Vous pouvez essayer l'option --repair pour réparer le ticket.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png b/output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png deleted file mode 100644 index 9411fc8..0000000 Binary files a/output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png and /dev/null differ diff --git a/output/ticket_T0140/attachments_info.json b/output/ticket_T0140/attachments_info.json deleted file mode 100644 index 7ffa6db..0000000 --- a/output/ticket_T0140/attachments_info.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "id": 31975, - "name": "ecran - 2020-04-07T120505.164.png", - "mimetype": "image/png", - "create_date": "2020-04-07 13:16:52", - "file_path": "output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png" - } -] \ No newline at end of file diff --git a/output/ticket_T0140/messages.json b/output/ticket_T0140/messages.json deleted file mode 100644 index d6d8a28..0000000 --- a/output/ticket_T0140/messages.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Problème sur numérotation de lots de béton qui s'effectue par unité !", - "code": "T0140", - "description": "

Problème sur numérotation de lots qui s'effectue par unité !

Serait-il possible de remédier à cela ?

Tél : 06.20.90.27.57

Plage horaire : 9h / 17h

Réf. GestCom : D006435

Origine : Logiciel

", - "date_create": "2020-04-07 13:15:23", - "role": "system", - "type": "contexte", - "body": "TICKET T0140: Problème sur numérotation de lots de béton qui s'effectue par unité !.\n\nDESCRIPTION:

Problème sur numérotation de lots qui s'effectue par unité !

Serait-il possible de remédier à cela ?

Tél : 06.20.90.27.57

Plage horaire : 9h / 17h

Réf. GestCom : D006435

Origine : Logiciel

" - }, - { - "id": "11136", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-10 15:15:54", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !", - "body": "Bonjour, Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...) Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du" - } -] \ No newline at end of file diff --git a/output/ticket_T0140/messages.json.backup b/output/ticket_T0140/messages.json.backup deleted file mode 100644 index 53a00b5..0000000 --- a/output/ticket_T0140/messages.json.backup +++ /dev/null @@ -1,59 +0,0 @@ -[ - { - "id": 11137, - "body": "", - "date": "2020-04-10 15:15:58", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11072, - "[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !" - ] - }, - { - "id": 11136, - "body": "

Bonjour,

Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...)
Je reste à votre disposition pour toute explication ou demande supplémentaire.
L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.
Cordialement.

", - "date": "2020-04-10 15:15:54", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !", - "parent_id": [ - 11072, - "[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !" - ] - }, - { - "id": 11088, - "body": "", - "date": "2020-04-09 09:49:57", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11072, - "[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !" - ] - }, - { - "id": 11072, - "body": "", - "date": "2020-04-07 13:15:23", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": false - } -] \ No newline at end of file diff --git a/output/ticket_T0140/questions_reponses.md b/output/ticket_T0140/questions_reponses.md deleted file mode 100644 index 1774812..0000000 --- a/output/ticket_T0140/questions_reponses.md +++ /dev/null @@ -1,15 +0,0 @@ -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 \ No newline at end of file diff --git a/output/ticket_T0140/rapport/ticket_analysis.json b/output/ticket_T0140/rapport/ticket_analysis.json deleted file mode 100644 index 199e599..0000000 --- a/output/ticket_T0140/rapport/ticket_analysis.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "entries": [ - { - "timestamp": "2025-04-01 17:31:17", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T0140/attachments/31975_ecran - 2020-04-07T120505.164.png", - "response": { - "pertinente": false, - "type_image": "autre", - "description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.", - "confiance": 90, - "justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:31:17", - "action": "extract_questions_reponses", - "agent": "AgentQuestionReponse", - "llm": { - "model": "mistral-medium" - }, - "parametres_llm": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "response": { - "success": true, - "messages_analyses": [ - { - "id": 11136, - "date": "2020-04-10 15:15:54", - "role": "Support", - "type": "Information", - "contenu": "Bonjour, Oui en effet l'ordre de tri d'affichage par défaut du tableau est par ordre alphabétique car certains de nos clients préfèrent utiliser une numérotation par lettre (A, B, C, D,..) pour identifier les numéros de lot. Si vous souhaitez identifier vos lots par numéro il vous suffit alors de mettre deux ou trois 0 devant chaque numéro de lot (ex: 0001, 0002, 0003,0004,...) Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du" - } - ], - "paires_qr": [], - "nb_questions": 0, - "nb_reponses": 0, - "tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000", - "parametres_llm": { - "agent": "AgentQuestionReponse", - "llm_type": "Mistral", - "parametres": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n", - "temperature": 0.3, - "max_tokens": 2000 - } - } - } - } - ] -} \ No newline at end of file diff --git a/output/ticket_T0140/rapport/ticket_analysis.md b/output/ticket_T0140/rapport/ticket_analysis.md deleted file mode 100644 index 13bbdc7..0000000 --- a/output/ticket_T0140/rapport/ticket_analysis.md +++ /dev/null @@ -1,76 +0,0 @@ -# Analyse de ticket de support - -## Statistiques - -- Images analysées: 1 (0 pertinentes) -- Questions identifiées: 0 -- Réponses identifiées: 0 - - -## Paramètres LLM par agent - -### Agent de filtrage d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.2 -- **Tokens max**: 500 - -### Agent d'analyse d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.3 -- **Tokens max**: 1024 - -### Agent d'extraction questions-réponses - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - - -## Journal d'actions - -### 2025-04-01 17:31:17 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 31975_ecran - 2020-04-07T120505.164.png -**Résultat**: Non pertinente -**Type**: autre -**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:31:17 - AgentQuestionReponse (mistral-medium) -**Action**: extract_questions_reponses -**Questions**: 0 -**Réponses**: 0 - - -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 - -**Paramètres LLM utilisés:** -- **model**: mistral-medium -- **temperature**: 0.3 -- **max_tokens**: 2000 -- **top_p**: 1.0 - ---- diff --git a/output/ticket_T0140/ticket_info.json b/output/ticket_T0140/ticket_info.json deleted file mode 100644 index d69cf1e..0000000 --- a/output/ticket_T0140/ticket_info.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "id": 152, - "active": true, - "name": "Problème sur numérotation de lots de béton qui s'effectue par unité !", - "description": "

Problème sur numérotation de lots qui s'effectue par unité !

Serait-il possible de remédier à cela ?

Tél : 06.20.90.27.57

Plage horaire : 9h / 17h

Réf. GestCom : D006435

Origine : Logiciel

", - "sequence": 11, - "stage_id": [ - 8, - "Clôturé" - ], - "kanban_state": "normal", - "create_date": "2020-04-07 13:15:23", - "write_date": "2024-10-03 13:10:50", - "date_start": "2020-04-07 09:24:00", - "date_end": false, - "date_assign": "2020-04-07 13:15:23", - "date_deadline": false, - "date_last_stage_update": "2020-04-10 15:15:57", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 9, - "Youness BENDEQ" - ], - "partner_id": [ - 7732, - "CONSEIL DEPARTEMENTAL DE HAUTE SAVOIE (74), Patrick LAVY" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "patrick.lavy@hautesavoie.fr", - "email_cc": false, - "working_hours_open": 0.0, - "working_hours_close": 0.0, - "working_days_open": 0.0, - "working_days_close": 0.0, - "website_message_ids": [ - 11136 - ], - "remaining_hours": 0.0, - "effective_hours": 0.0, - "total_hours_spent": 0.0, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [], - "priority": "0", - "code": "T0140", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 10888 - ], - "message_ids": [ - 11137, - 11136, - 11088, - 11072 - ], - "message_main_attachment_id": [ - 31975, - "ecran - 2020-04-07T120505.164.png" - ], - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "e2a9af6a-dc90-4892-82b7-33fa5d062d75", - "create_uid": [ - 9, - "Youness BENDEQ" - ], - "write_uid": [ - 1, - "OdooBot" - ], - "x_CBAO_windows_maj_ID": false, - "x_CBAO_version_signalement": false, - "x_CBAO_version_correction": false, - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "youness@cbao.fr", - "attachment_ids": [ - 31975 - ], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 1, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/152", - "access_warning": "", - "display_name": "[T0140] Problème sur numérotation de lots de béton qui s'effectue par unité !", - "__last_update": "2024-10-03 13:10:50" -} \ No newline at end of file diff --git a/output/ticket_T0150/attachments_info.json b/output/ticket_T0150/attachments_info.json deleted file mode 100644 index 0637a08..0000000 --- a/output/ticket_T0150/attachments_info.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/output/ticket_T0150/messages.json b/output/ticket_T0150/messages.json deleted file mode 100644 index 3ecec09..0000000 --- a/output/ticket_T0150/messages.json +++ /dev/null @@ -1,168 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Problème d'impression des PV de compression des éprouvettes 11H22", - "code": "T0150", - "description": "

La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.

", - "date_create": "2020-04-14 12:41:38", - "role": "system", - "type": "contexte", - "body": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22.\n\nDESCRIPTION:

La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.

" - }, - { - "id": "ticket_info", - "author_id": [ - 0, - "" - ], - "role": "Client", - "type": "Question", - "date": "", - "email_from": "", - "subject": "", - "body": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22. DESCRIPTION: La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée." - }, - { - "id": "11183", - "author_id": [ - 0, - "" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-14 12:39:49", - "email_from": "Monsieur Yoan Cazard ", - "subject": "Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2.", - "body": "Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour" - }, - { - "id": "11291", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-20 16:40:40", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11296", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-21 08:29:06", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11304", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-21 14:57:48", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Etes vous disponible demain dans la matinée ? Je reste" - }, - { - "id": "11305", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-21 15:03:22", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste" - }, - { - "id": "11309", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-22 08:22:21", - "email_from": "\"Youness BENDEQ\" ", - "subject": "False", - "body": "Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ?" - }, - { - "id": "11320", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-23 09:46:11", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11321", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-23 10:33:06", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste" - }, - { - "id": "11322", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-23 12:33:46", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - }, - { - "id": "11323", - "author_id": [ - 0, - "" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-23 12:59:43", - "email_from": "Support Technique CBAO ", - "subject": "échanges de mail entre M. Pierre PAYA du Cerema d'Aix et M.Dierkens du cerema de LYON", - "body": "-------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - }, - { - "id": "11462", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-05-06 11:36:50", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Appeler fait par Youness BENDEQ Pour savoir si la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée a été ajouté. Commentaire Possibilité ajoutée" - } -] \ No newline at end of file diff --git a/output/ticket_T0150/messages.json.backup b/output/ticket_T0150/messages.json.backup deleted file mode 100644 index 72719f2..0000000 --- a/output/ticket_T0150/messages.json.backup +++ /dev/null @@ -1,293 +0,0 @@ -[ - { - "id": 11463, - "body": "", - "date": "2020-05-06 11:37:11", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11462, - "body": "
\n

\n Appeler fait\n par Youness BENDEQ\n \n

\n

Pour savoir si la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée a été ajouté.

\n
\n Commentaire\n

Possibilité ajoutée

\n
\n
\n ", - "date": "2020-05-06 11:36:50", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11323, - "body": "
\r\n
\r\n

\r\n
\r\n -------- Message transféré --------\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
Sujet :\r\n Re: [T0150] Problème d'impression des PV de compression\r\n des éprouvettes 11H22
Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST)
De : CAZARD Yoan <yoan.cazard@cd66.fr>
Pour : support <support@cbao.fr>
\r\n
\r\n
\r\n
\r\n
Oui, il faut prendre la valeur corrigée (selon EN 206/CN)\r\n pour évaluer la conformité du béton.
\r\n Il faut bien distinguer :
\r\n - la norme d'essai 12390-3 qui ne traite que de l'essai et qui\r\n n'a pas vocation à se prononcer sur la conformité du matériau\r\n : le résultat d'essai est bien la force appliquée (en N)\r\n divisée par la section de l'éprouvette, quelle que soit cette\r\n section,
\r\n - la norme béton EN 206/CN, qui donne des indications pour se\r\n prononcer sur la conformité du béton, notamment la prise en\r\n compte de la taille de éprouvette (11x22) et des corrections à\r\n apporter pour se ramener à une éprouvette normalisée 15x30.
\r\n
\r\n Le mode de correction des valeurs brutes d'essai est\r\n indépendant de la norme d'essai et vous appartient.
\r\n Dans le cas de carottes prélevées sur ouvrages existant par\r\n exemple, la correction ne se fera pas selon l'EN 206/CN mais\r\n selon d'autres dispositions.
\r\n
\r\n cdlt
\r\n
Pierre PAYA
\r\n
\r\n
Responsable de l'activité\r\n Matériaux, Construction et Certification
\r\n
\r\n Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.:\r\n 04 42 24 78 30
\r\n \r\n Direction Territoriale Méditerranée
\r\n
\"\"
\r\n
Centre d’études et d’expertise\r\n sur les risques, l’environnement, la mobilité et\r\n l’aménagement
\r\n www.cerema.fr
\r\n

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Le 23/04/2020 à 10:36, >\r\n CAZARD Yoan (par Internet) a écrit :
\r\n
\r\n
\r\n
\r\n
Bonjour et merci beaucoup Pierre pour ces précisions.\r\n C'est plus clair.
\r\n
\r\n

\r\n
\r\n
Donc, on peut bien avoir, comme ce qui se faisait\r\n déjà, les deux valeurs affichées sur le rapport\r\n d'essais.
\r\n
\r\n

\r\n
\r\n
Par contre, pour la vérification de résistances\r\n d'éprouvettes béton, pour des laboratoires\r\n départementaux comme nous, et notamment pour de petites\r\n valeurs (pour le RAANE par exemple), pouvons-nous\r\n continuer à prendre les résistances corrigées pour la\r\n conformité du matériau?
\r\n
\r\n

\r\n
\r\n
Yoan CAZARD
\r\n Responsable Laboratoire Départemental Routes et Ouvrages\r\n d'Art
\r\n Conseil Départemental 66
\r\n Direction Infrastructures et Déplacements
\r\n Développement et Expertise Technique
\r\n 1265 avenue Julien Panchot
\r\n 66000 Perpignan
\r\n 04-68-68-36-90
\r\n 06-73-87-52-43
\r\n

\r\n
\r\n
\r\n
De: \"PAYA Pierre\" <Pierre.Paya@cerema.fr>
\r\n À: \"CAZARD Yoan\" <yoan.cazard@cd66.fr>
\r\n Cc: \"JAN Didier\" <didier.jan@cerema.fr>,\r\n \"ddumas\" <ddumas@ardeche.fr>,\r\n \"Gérald LACROIX\" <glacroix@cg83.fr>,\r\n \"s labourasse\" <s.labourasse@cg04.fr>,\r\n \"Stephane roques\" <Stephane.roques@cg12.fr>,\r\n \"claude trelcat\" <claude.trelcat@cg84.fr>,\r\n \"benoit gaumet\" <benoit.gaumet@cg12.fr>,\r\n \"jm dao\" <jm.dao@cg04.fr>,\r\n \"a ferrer\" <a.ferrer@cg04.fr>,\r\n \"pierre david\" <pierre.david@cg05.fr>,\r\n \"philippe delorme\" <philippe.delorme@cg05.fr>,\r\n \"Morales Frederic\" <frederic.morales@vaucluse.fr>,\r\n \"CELLI Sylvain\" <sylvain.celli@cg-corsedusud.fr>,\r\n \"BOISSONNADE-CORP Pierre\" <pierre.boissonnade-corp@cg12.fr>,\r\n \"Richard BOREL\" <richard.borel@cg05.fr>,\r\n \"alain guiraud\" <alain.guiraud@cg12.fr>,\r\n \"raymond cayzac\" <raymond.cayzac@cg12.fr>,\r\n \"georges pouget\" <georges.pouget@cg12.fr>,\r\n \"noel sanchez\" <noel.sanchez@ct-corse.fr>,\r\n \"joseph pau\" <joseph.pau@cg-corsedusud.fr>,\r\n \"nicolas teisseire\" <nicolas.teisseire@cg11.fr>,\r\n mmohammedi@ardeche.fr,\r\n \"jerome gombault\" <jerome.gombault@cg11.fr>,\r\n \"alain nargeot\" <alain.nargeot@gard.fr>,\r\n \"bhilaire\" <bhilaire@ardeche.fr>,\r\n \"TEKATLIAN Annick\" <Annick.TEKATLIAN@cerema.fr>,\r\n \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" <Nathalie.Cordier@cerema.fr>,\r\n \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" <lisa.gontard@cerema.fr>
\r\n Envoyé: Jeudi 23\r\n Avril 2020 10:17:43
\r\n Objet: Re: Ecrasements béton
\r\n
\r\n

\r\n
\r\n
bonjour à tous,
\r\n
\r\n Je viens d'avoir Michael  Dierkens au téléphone et il\r\n m'a retransmis la réponse faite à BRG-Lab :
\r\n \"Si une seule valeur apparait, çà ne peut\r\n être que la valeur brute (non-corrigée).
\r\n Une autre solution peut être aussi, s'il y a assez de\r\n place sur le rapport d'essai, de faire apparaitre 2\r\n colonnes : 1 corrigée et 1 non-corrigée. Mais la\r\n valeur corrigée peut être vue comme une interprétation\r\n du résultat et poser des problèmes à un labo qui\r\n serait Cofrac.\"
\r\n
\r\n Sa réponse est cohérente (heureusement\r\n ...) avec celle que j'avais faite en décembre\r\n 2019 :
\r\n
\"Je rajoute également\r\n que doivent figurer dans le PV d'essais :
\r\n - la charge maximale à la rupture en N,
\r\n - la résistance à la compression non\r\n corrigée en Mpa (c'est à dire sans prise en\r\n compte des corrections de l'EN 206 sur les 11x22\r\n notamment).\"
\r\n
\r\n Il faut retenir que :
\r\n - la norme impose d'afficher dans le PV la charge\r\n maximale à rupture non corrigée (en N) ainsi que la\r\n résistance non corrigée (en MPa) : ces données\r\n doivent donc figurer impérativement dans le PV,
\r\n - la norme n'interdit pas de rajouter des éléments\r\n dans le PV, en particulier la résistance corrigée\r\n pour des 11x22, si cette donnée vous parait utile\r\n (ou pour votre client) : dans ce cas le COFRAC peut\r\n considérer que l'affichage de cette valeur corrigée\r\n est une interprétation du résultat et demander à\r\n vérifier que l'interprétation des résultats a bien\r\n été définie clairement dans le contrat qui vous lie\r\n à votre client.
\r\n
\r\n Pour information nous faisons apparaitre les\r\n résistances brutes et corrigées sur nos PV (sous\r\n accréditation COFRAC).
\r\n
\r\n Je reste disponible pour toute précision.
\r\n
\r\n bien cordialement,
\r\n
\r\n
Pierre PAYA
\r\n
\r\n
\r\n Responsable de l'activité Matériaux,\r\n Construction et Certification
\r\n
Laboratoire -\r\n service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30
\r\n Direction Territoriale\r\n Méditerranée
\r\n
\"\"
\r\n
Centre\r\n d’études et d’expertise sur les risques,\r\n l’environnement, la mobilité et l’aménagement\r\n
\r\n www.cerema.fr
\r\n

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Le 22/04/2020 à 10:11, >\r\n CAZARD Yoan (par Internet) a écrit :
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n

Bonjour à tous,
\r\n

\r\n


\r\n

\r\n

J'espère que tout\r\n le monde va bien dans ce contexte particulier.

\r\n


\r\n

\r\n

Je vous fait\r\n remonter une problématique que nous avons\r\n rencontrée en passant sur la version web de\r\n BRG-LAB. Cela concerne les résultats d'essais\r\n des écrasements des éprouvettes 11x22. Les PV\r\n ou rapports d'essais n'affichent plus que les\r\n résistances brutes sans la correction de 1\r\n MPa. Je vous joins ci-dessous la réponse de\r\n CBAO. Je les ai contactés depuis afin de\r\n pouvoir ajouter une option à cocher pour\r\n choisir d'afficher cette résistance corrigée.\r\n Je pense que nous sommes nombreux dans ce cas.\r\n Nous sommes dans l'attente d'une solution de\r\n CBAO.

\r\n


\r\n

\r\n

Pierre Paya nous\r\n avait déjà indiqué de n'afficher que la\r\n résistance brute sur le PV d'essais, mais il\r\n me semble que nous prenons tous en compte que\r\n la résistance corrigée?
\r\n

\r\n


\r\n

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n

\r\n
\r\n
Yoan CAZARD
\r\n Responsable Laboratoire Départemental Routes et Ouvrages d'Art
\r\n Conseil Départemental 66
\r\n Direction Infrastructures et Déplacements
\r\n Développement et Expertise Technique
\r\n 1265 avenue Julien Panchot
\r\n 66000 Perpignan
\r\n 04-68-68-36-90
\r\n 06-73-87-52-43
\r\n
\r\n
\r\n
De: \"Youness BENDEQ\"\r\n <youness.bendeq@cbao.fr>
\r\n À: \"Yohan CAZARD\" <yoan.cazard@cd66.fr>
\r\n Envoyé: Jeudi 23 Avril 2020 12:33:06
\r\n Objet: Re: [T0150] Problème d'impression des PV de\r\n compression des éprouvettes 11H22
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
Voir\r\n Tâche \"CBAO\r\n
\r\n
\r\n
\r\n
\r\n

Bonjour,
\r\n

\r\n


\r\n

\r\n
Ci-dessous les\r\n échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis\r\n en relation avec M.Dierkens.
\r\n


\r\n

\r\n

Je ne vois pas l'échange\r\n complet, pouvez-vous me le renvoyer svp ?
\r\n

\r\n

Je reste à votre disposition\r\n pour toute explication ou demande supplémentaire.
\r\n

\r\n

L'objectif du Support\r\n Technique est de vous aider : n'hésitez jamais à nous\r\n contacter si vous rencontrez une difficulté, ou pour\r\n nous soumettre une ou des suggestions d'amélioration de\r\n nos logiciels ou de nos méthodes.
\r\n

\r\n

Cordialement.
\r\n

\r\n


\r\n

\r\n

Support Technique - CBAO
\r\n

\r\n

www.cbao.fr
\r\n

\r\n

80 rue Louis Braille
\r\n

\r\n

66000 PERPIGNAN
\r\n

\r\n

support@cbao.fr
\r\n

\r\n

Tél : 04 68 64 15 31
\r\n

\r\n

Fax : 04 68 64 31 69
\r\n

\r\n
\r\n
\r\n


\r\n

\r\n
\r\n

Envoyé \r\n par CBAO\r\n S.A.R.L. .\r\n

\r\n
\r\n \"\"
\r\n
\r\n
\r\n
\r\n
\r\n", - "date": "2020-04-23 12:59:43", - "author_id": false, - "email_from": "Support Technique CBAO ", - "subject": "échanges de mail entre M. Pierre PAYA du Cerema d'Aix et M.Dierkens du cerema de LYON", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11322, - "body": "
Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton.
Il faut bien distinguer :
- la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section,
- la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30.

Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient.
Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions.

cdlt
Pierre PAYA
Responsable de l'activité Matériaux, Construction et Certification
Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30
Direction Territoriale Méditerranée
\"\"
Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement
www.cerema.fr

Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit :
Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair.

Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais.

Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau?

Yoan CAZARD
Responsable Laboratoire Départemental Routes et Ouvrages d'Art
Conseil Départemental 66
Direction Infrastructures et Déplacements
Développement et Expertise Technique
1265 avenue Julien Panchot
66000 Perpignan
04-68-68-36-90
06-73-87-52-43


De: \"PAYA Pierre\" <Pierre.Paya@cerema.fr>
À: \"CAZARD Yoan\" <yoan.cazard@cd66.fr>
Cc: \"JAN Didier\" <didier.jan@cerema.fr>, \"ddumas\" <ddumas@ardeche.fr>, \"Gérald LACROIX\" <glacroix@cg83.fr>, \"s labourasse\" <s.labourasse@cg04.fr>, \"Stephane roques\" <Stephane.roques@cg12.fr>, \"claude trelcat\" <claude.trelcat@cg84.fr>, \"benoit gaumet\" <benoit.gaumet@cg12.fr>, \"jm dao\" <jm.dao@cg04.fr>, \"a ferrer\" <a.ferrer@cg04.fr>, \"pierre david\" <pierre.david@cg05.fr>, \"philippe delorme\" <philippe.delorme@cg05.fr>, \"Morales Frederic\" <frederic.morales@vaucluse.fr>, \"CELLI Sylvain\" <sylvain.celli@cg-corsedusud.fr>, \"BOISSONNADE-CORP Pierre\" <pierre.boissonnade-corp@cg12.fr>, \"Richard BOREL\" <richard.borel@cg05.fr>, \"alain guiraud\" <alain.guiraud@cg12.fr>, \"raymond cayzac\" <raymond.cayzac@cg12.fr>, \"georges pouget\" <georges.pouget@cg12.fr>, \"noel sanchez\" <noel.sanchez@ct-corse.fr>, \"joseph pau\" <joseph.pau@cg-corsedusud.fr>, \"nicolas teisseire\" <nicolas.teisseire@cg11.fr>, mmohammedi@ardeche.fr, \"jerome gombault\" <jerome.gombault@cg11.fr>, \"alain nargeot\" <alain.nargeot@gard.fr>, \"bhilaire\" <bhilaire@ardeche.fr>, \"TEKATLIAN Annick\" <Annick.TEKATLIAN@cerema.fr>, \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" <Nathalie.Cordier@cerema.fr>, \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" <lisa.gontard@cerema.fr>
Envoyé: Jeudi 23 Avril 2020 10:17:43
Objet: Re: Ecrasements béton

bonjour à tous,

Je viens d'avoir Michael  Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab :
\"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée).
Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\"

Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 :
\"Je rajoute également que doivent figurer dans le PV d'essais :
- la charge maximale à la rupture en N,
- la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\"

Il faut retenir que :
- la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV,
- la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client.

Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC).

Je reste disponible pour toute précision.

bien cordialement,
Pierre PAYA
Responsable de l'activité Matériaux, Construction et Certification
Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30
Direction Territoriale Méditerranée
\"\"
Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement
www.cerema.fr

Le 22/04/2020 à 10:11, > CAZARD Yoan (par Internet) a écrit :

Bonjour à tous,


J'espère que tout le monde va bien dans ce contexte particulier.


Je vous fait remonter une problématique que nous avons rencontrée en passant sur la version web de BRG-LAB. Cela concerne les résultats d'essais des écrasements des éprouvettes 11x22. Les PV ou rapports d'essais n'affichent plus que les résistances brutes sans la correction de 1 MPa. Je vous joins ci-dessous la réponse de CBAO. Je les ai contactés depuis afin de pouvoir ajouter une option à cocher pour choisir d'afficher cette résistance corrigée. Je pense que nous sommes nombreux dans ce cas. Nous sommes dans l'attente d'une solution de CBAO.


Pierre Paya nous avait déjà indiqué de n'afficher que la résistance brute sur le PV d'essais, mais il me semble que nous prenons tous en compte que la résistance corrigée?



Yoan CAZARD
Responsable Laboratoire Départemental Routes et Ouvrages d'Art
Conseil Départemental 66
Direction Infrastructures et Déplacements
Développement et Expertise Technique
1265 avenue Julien Panchot
66000 Perpignan
04-68-68-36-90
06-73-87-52-43


De: \"Youness BENDEQ\" <youness.bendeq@cbao.fr>
À: \"Yohan CAZARD\" <yoan.cazard@cd66.fr>
Envoyé: Jeudi 23 Avril 2020 12:33:06
Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22

\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n Voir Tâche\r\n \r\n \r\n \r\n \"CBAO\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Bonjour,


Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens.


Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ?

Je reste à votre disposition pour toute explication ou demande supplémentaire.

L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.

Cordialement.


Support Technique - CBAO

www.cbao.fr

80 rue Louis Braille

66000 PERPIGNAN

support@cbao.fr

Tél : 04 68 64 15 31

Fax : 04 68 64 31 69 \t

\r\n\r\n


\r\n

\r\n Envoyé\r\n \r\n par\r\n \r\n CBAO S.A.R.L.\r\n \r\n \r\n \r\n .\r\n

\r\n
\r\n \r\n\"\"
", - "date": "2020-04-23 12:33:46", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11321, - "body": "

Bonjour,


Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens.


Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ?

Je reste à votre disposition pour toute explication ou demande supplémentaire.

L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.

Cordialement.


Support Technique - CBAO

www.cbao.fr

80 rue Louis Braille

66000 PERPIGNAN

support@cbao.fr

Tél : 04 68 64 15 31

Fax : 04 68 64 31 69 \t

", - "date": "2020-04-23 10:33:06", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11320, - "body": "
Bonjour,

Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens.

C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais.

Yoan CAZARD
Responsable Laboratoire Départemental Routes et Ouvrages d'Art
Conseil Départemental 66
Direction Infrastructures et Déplacements
Développement et Expertise Technique
1265 avenue Julien Panchot
66000 Perpignan
04-68-68-36-90
06-73-87-52-43


De: \"Youness BENDEQ\" <youness.bendeq@cbao.fr>
À: \"Yohan CAZARD\" <yoan.cazard@cd66.fr>
Envoyé: Lundi 20 Avril 2020 18:40:40
Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22

\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n Voir Tâche\r\n \r\n \r\n \r\n \"CBAO\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Bonjour,

Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée).

En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque.

C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN.


La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel).


Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous.


Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme.

Je reste à votre disposition pour toute explication ou demande supplémentaire.

L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.

Cordialement.

Support Technique - CBAO

www.cbao.fr

80 rue Louis Braille

66000 PERPIGNAN

support@cbao.fr

Tél : 04 68 64 15 31

Fax : 04 68 64 31 69 \t

\r\n\r\n


\r\n

\r\n Envoyé\r\n \r\n par\r\n \r\n CBAO S.A.R.L.\r\n \r\n \r\n \r\n .\r\n

\r\n
\r\n \r\n\"\"
", - "date": "2020-04-23 09:46:11", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11310, - "body": "", - "date": "2020-04-22 08:24:29", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11309, - "body": "

Salut Laurent,

> Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir.

Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup.

Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution.

En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique.

Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa.

Or avec la résistance brute, il obtient une non conformité.

Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date.

C'est faisable ?

", - "date": "2020-04-22 08:22:21", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11305, - "body": "
Oui pas de souci merci.

Yoan CAZARD
Responsable Laboratoire Départemental Routes et Ouvrages d'Art
Conseil Départemental 66
Direction Infrastructures et Déplacements
Développement et Expertise Technique
1265 avenue Julien Panchot
66000 Perpignan
04-68-68-36-90
06-73-87-52-43


De: \"Youness BENDEQ\" <youness.bendeq@cbao.fr>
À: \"Yohan CAZARD\" <yoan.cazard@cd66.fr>
Envoyé: Mardi 21 Avril 2020 16:57:48
Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22

\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n Voir Tâche\r\n \r\n \r\n \r\n \"CBAO\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Bonjour,
Etes vous disponible demain dans la matinée ?
Je reste à votre disposition pour toute explication ou demande supplémentaire.
L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.
Cordialement.

Support Technique - CBAO
www.cbao.fr
80 rue Louis Braille
66000 PERPIGNAN
support@cbao.fr
Tél : 04 68 64 15 31
Fax : 04 68 64 31 69

\r\n\r\n


\r\n

\r\n Envoyé\r\n \r\n par\r\n \r\n CBAO S.A.R.L.\r\n \r\n \r\n \r\n .\r\n

\r\n
\r\n \r\n\"\"
", - "date": "2020-04-21 15:03:22", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11304, - "body": "

Bonjour,
Etes vous disponible demain dans la matinée ?
Je reste à votre disposition pour toute explication ou demande supplémentaire.
L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.
Cordialement.

Support Technique - CBAO
www.cbao.fr
80 rue Louis Braille
66000 PERPIGNAN
support@cbao.fr
Tél : 04 68 64 15 31
Fax : 04 68 64 31 69

", - "date": "2020-04-21 14:57:48", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11296, - "body": "
Bonjour,

Vous pouvez me rappeler?

Yoan CAZARD
Responsable Laboratoire Départemental Routes et Ouvrages d'Art
Conseil Départemental 66
Direction Infrastructures et Déplacements
Développement et Expertise Technique
1265 avenue Julien Panchot
66000 Perpignan
04-68-68-36-90
06-73-87-52-43


De: \"Youness BENDEQ\" <youness.bendeq@cbao.fr>
À: \"Yohan CAZARD\" <yoan.cazard@cd66.fr>
Envoyé: Lundi 20 Avril 2020 18:40:40
Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22

\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n Voir Tâche\r\n \r\n \r\n \r\n \"CBAO\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Bonjour,

Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée).

En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque.

C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN.


La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel).


Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous.


Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme.

Je reste à votre disposition pour toute explication ou demande supplémentaire.

L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.

Cordialement.

Support Technique - CBAO

www.cbao.fr

80 rue Louis Braille

66000 PERPIGNAN

support@cbao.fr

Tél : 04 68 64 15 31

Fax : 04 68 64 31 69 \t

\r\n\r\n


\r\n

\r\n Envoyé\r\n \r\n par\r\n \r\n CBAO S.A.R.L.\r\n \r\n \r\n \r\n .\r\n

\r\n
\r\n \r\n\"\"
", - "date": "2020-04-21 08:29:06", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11291, - "body": "

Bonjour,

Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée).

En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque.

C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN.


La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel).


Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous.


Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme.

Je reste à votre disposition pour toute explication ou demande supplémentaire.

L'objectif du Support Technique est de vous aider : n'hésitez jamais à nous contacter si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes.

Cordialement.

Support Technique - CBAO

www.cbao.fr

80 rue Louis Braille

66000 PERPIGNAN

support@cbao.fr

Tél : 04 68 64 15 31

Fax : 04 68 64 31 69 \t

", - "date": "2020-04-20 16:40:40", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11279, - "body": "", - "date": "2020-04-20 13:45:40", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11244, - "body": "", - "date": "2020-04-20 07:01:28", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11190, - "body": "", - "date": "2020-04-14 12:54:18", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11189, - "body": "", - "date": "2020-04-14 12:53:27", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11188, - "body": "", - "date": "2020-04-14 12:53:21", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11185, - "body": "", - "date": "2020-04-14 12:41:38", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11184, - "body": "", - "date": "2020-04-14 12:41:38", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11183, - "body": "

Notification d'appel\r\n\r\n\r\n\r\n

\r\n\r\n\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\r\n
 \r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\r\n\t\t\t
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t    \r\n\t\t\t\t\t\t\t\t\t

Version imprimable

\r\n\t\t\t\t\t\t\t\t\t

Notification d'appel

\r\n\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t
Nous avons reçu un appel pour support technique (CBAO) pour le numéro de téléphone +33 / 4 - 11980441.
 
 
 
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
Date: Mardi, 14. avril 2020, 14:37
 
Appel de: Monsieur Yoan Cazard, Conseil départemental du 66
 
\r\n\t\t\t\t\t\t\t\t\tTéléphone principal: \r\n\t\t\t\t\t\t\t\t\t\t+33673875243\r\n\t\t\t\t\t\t\t\t
 
Sujet d'appel: Demande technique
 
\r\n\t\t\t\t\t
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\tEditer les données de l'appelant\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t
 
 
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
 \r\n\t\t\t\t\t\t\t\t\tMonsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2. \r\n\t\t\t\t\t\t\t\t 
\r\n\t\t\t\t\t
 
 
 
Nous attendons vos commentaires, afin de nous assurer que vos secrétaires préférés prennent en charge vos appels.
 
\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t      Ne convient       
      pas      \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t      Pas       
 satisfaisant(e) \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t         Correct(e)         
 \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t        Satisfaisant(e)        
 \r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"\"\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tSecrétaire
préférée\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t\t\t\t
\r\n\t\t\t\t\t
 
\r\n\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t
 
\r\n\t\t\t\t\t\t\tParrainez un nouveau client
Votre secrétariat bureau24 vous donne satisfaction ? N’attendez plus et recommandez-nous auprès des professionnels de votre réseau! ! Pour chaque parrainage vous bénéficierez d’un bonus de 40 euros sur les frais de consommation. Complétez facilement en ligne le formulaire sécurisé: www.bureau24.fr/parrainage. N'hésitez pas à nous contacter si vous avez la moindre question au 0805 965 770.\r\n\t\t\t\t\t\t
 
\r\n\t\t\t\t\t\t\tVous souhaitez utiliser plus de fonctions pour gérer votre secrétariat ou nous suggérer des améliorations quant à celles existantes? Le travail de notre équipe de développeurs dépend de vos commentaires, n'hésitez pas à nous envoyer vos commentaires à feedback@bureau24.fr\r\n\t\t\t\t\t\t
 
\r\n\t\t
 
\r\n\r\n\r\n", - "date": "2020-04-14 12:39:49", - "author_id": false, - "email_from": "Monsieur Yoan Cazard ", - "subject": "Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2.", - "parent_id": [ - 11182, - "[T0150] Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2." - ] - }, - { - "id": 11182, - "body": "", - "date": "2020-04-14 12:41:38", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - } -] \ No newline at end of file diff --git a/output/ticket_T0150/messages.json.original b/output/ticket_T0150/messages.json.original deleted file mode 100644 index cc00756..0000000 --- a/output/ticket_T0150/messages.json.original +++ /dev/null @@ -1,155 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Problème d'impression des PV de compression des éprouvettes 11H22", - "code": "T0150", - "description": "

La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.

", - "date_create": "2020-04-14 12:41:38", - "role": "system", - "type": "contexte", - "body": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22.\n\nDESCRIPTION:

La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.

" - }, - { - "id": "11183", - "author_id": [ - 0, - "" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-14 12:39:49", - "email_from": "Monsieur Yoan Cazard ", - "subject": "Monsieur Cazard souhaite que vous le rappeliez. Il indique avoir un problème au niveau de la moyenne sur des écrasements 11M2.", - "body": "Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour" - }, - { - "id": "11291", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-20 16:40:40", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11296", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-21 08:29:06", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11304", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-21 14:57:48", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Etes vous disponible demain dans la matinée ? Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du" - }, - { - "id": "11305", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-21 15:03:22", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste à votre disposition pour toute explication ou demande supplémentaire. L'objectif du" - }, - { - "id": "11309", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-22 08:22:21", - "email_from": "\"Youness BENDEQ\" ", - "subject": "False", - "body": "Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ?" - }, - { - "id": "11320", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-23 09:46:11", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - { - "id": "11321", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-23 10:33:06", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste" - }, - { - "id": "11322", - "author_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "role": "Client", - "type": "Question", - "date": "2020-04-23 12:33:46", - "email_from": "CAZARD Yoan ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - }, - { - "id": "11323", - "author_id": [ - 0, - "" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-23 12:59:43", - "email_from": "Support Technique CBAO ", - "subject": "échanges de mail entre M. Pierre PAYA du Cerema d'Aix et M.Dierkens du cerema de LYON", - "body": "-------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - }, - { - "id": "11462", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-05-06 11:36:50", - "email_from": "\"Youness BENDEQ\" ", - "subject": "Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "body": "Appeler fait par Youness BENDEQ Pour savoir si la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée a été ajouté. Commentaire Possibilité ajoutée" - } -] \ No newline at end of file diff --git a/output/ticket_T0150/questions_reponses.md b/output/ticket_T0150/questions_reponses.md deleted file mode 100644 index b1b15b1..0000000 --- a/output/ticket_T0150/questions_reponses.md +++ /dev/null @@ -1,20 +0,0 @@ -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| -| **Client**: Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour | **Support**: Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | -| **Client**: Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Etes vous disponible demain dans la matinée ? Je reste | -| **Client**: Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste | **Support**: Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ? | -| **Client**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste | -| **Client**: Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "PAYA Pierre" À: "CAZARD Yoan" Cc: "JAN Didier" , "ddumas" , "Gérald LACROIX" , "s labourasse" , "Stephane roques" , "claude trelcat" , "benoit gaumet" , "jm dao" , "a ferrer" , "pierre david" , "philippe delorme" , "Morales Frederic" , "CELLI Sylvain" , "BOISSONNADE-CORP Pierre" , "Richard BOREL" , "alain guiraud" , "raymond cayzac" , "georges pouget" , "noel sanchez" , "joseph pau" , "nicolas teisseire" , mmohammedi@ardeche.fr , "jerome gombault" , "alain nargeot" , "bhilaire" , "TEKATLIAN Annick" , "CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB" , "GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : "Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac." Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : " Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment)." Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien | **Support**: -------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "PAYA Pierre" À: "CAZARD Yoan" Cc: "JAN Didier" , "ddumas" , "Gérald LACROIX" , "s labourasse" , "Stephane roques" , "claude trelcat" , "benoit gaumet" , "jm dao" , "a ferrer" , "pierre david" , "philippe delorme" , "Morales Frederic" , "CELLI Sylvain" , "BOISSONNADE-CORP Pierre" , "Richard BOREL" , "alain guiraud" , "raymond cayzac" , "georges pouget" , "noel sanchez" , "joseph pau" , "nicolas teisseire" , mmohammedi@ardeche.fr , "jerome gombault" , "alain nargeot" , "bhilaire" , "TEKATLIAN Annick" , "CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB" , "GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : "Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac." Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : " Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment)." Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien | - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 \ No newline at end of file diff --git a/output/ticket_T0150/rapport/ticket_analysis.json b/output/ticket_T0150/rapport/ticket_analysis.json deleted file mode 100644 index 27f5797..0000000 --- a/output/ticket_T0150/rapport/ticket_analysis.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "entries": [ - { - "timestamp": "2025-04-01 17:30:53", - "action": "extract_questions_reponses", - "agent": "AgentQuestionReponse", - "llm": { - "model": "mistral-medium" - }, - "parametres_llm": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "response": { - "success": true, - "messages_analyses": [ - { - "id": "ticket_info", - "date": "", - "role": "Client", - "type": "Question", - "contenu": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22. DESCRIPTION: La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée." - }, - { - "id": "ticket_info", - "date": "", - "role": "Client", - "type": "Question", - "contenu": "TICKET T0150: Problème d'impression des PV de compression des éprouvettes 11H22. DESCRIPTION: La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée." - }, - { - "id": "11183", - "date": "2020-04-14 12:39:49", - "role": "Client", - "type": "Question", - "contenu": "Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour" - }, - { - "id": "11291", - "date": "2020-04-20 16:40:40", - "role": "Support", - "type": "Information", - "contenu": "Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avo" - }, - { - "id": "11296", - "date": "2020-04-21 08:29:06", - "role": "Client", - "type": "Question", - "contenu": "Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre de" - }, - { - "id": "11304", - "date": "2020-04-21 14:57:48", - "role": "Support", - "type": "Information", - "contenu": "Bonjour, Etes vous disponible demain dans la matinée ? Je reste" - }, - { - "id": "11305", - "date": "2020-04-21 15:03:22", - "role": "Client", - "type": "Question", - "contenu": "Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée" - }, - { - "id": "11309", - "date": "2020-04-22 08:22:21", - "role": "Support", - "type": "Information", - "contenu": "Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur" - }, - { - "id": "11320", - "date": "2020-04-23 09:46:11", - "role": "Client", - "type": "Question", - "contenu": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"You" - }, - { - "id": "11321", - "date": "2020-04-23 10:33:06", - "role": "Support", - "type": "Information", - "contenu": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste" - }, - { - "id": "11322", - "date": "2020-04-23 12:33:46", - "role": "Client", - "type": "Question", - "contenu": "Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en comp" - }, - { - "id": "11323", - "date": "2020-04-23 12:59:43", - "role": "Support", - "type": "Information", - "contenu": "-------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en" - }, - { - "id": "11462", - "date": "2020-05-06 11:36:50", - "role": "Support", - "type": "Information", - "contenu": "Appeler fait par Youness BENDEQ Pour savoir si la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée a été ajouté. Commentaire Possibilité ajoutée" - } - ], - "paires_qr": [ - { - "numero": "1", - "question": { - "role": "Client", - "contenu": "Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour" - }, - "reponse": { - "role": "Support", - "contenu": "Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - } - }, - { - "numero": "2", - "question": { - "role": "Client", - "contenu": "Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - "reponse": { - "role": "Support", - "contenu": "Bonjour, Etes vous disponible demain dans la matinée ? Je reste" - } - }, - { - "numero": "3", - "question": { - "role": "Client", - "contenu": "Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste" - }, - "reponse": { - "role": "Support", - "contenu": "Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ?" - } - }, - { - "numero": "4", - "question": { - "role": "Client", - "contenu": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste" - }, - "reponse": { - "role": "Support", - "contenu": "Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste" - } - }, - { - "numero": "5", - "question": { - "role": "Client", - "contenu": "Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - }, - "reponse": { - "role": "Support", - "contenu": "-------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien" - } - } - ], - "nb_questions": 5, - "nb_reponses": 5, - "tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n| **Client**: Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour | **Support**: Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste |\n| **Client**: Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Etes vous disponible demain dans la matinée ? Je reste |\n| **Client**: Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste | **Support**: Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ? |\n| **Client**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"Youness BENDEQ\" À: \"Yohan CAZARD\" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste |\n| **Client**: Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien | **Support**: -------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: \"PAYA Pierre\" À: \"CAZARD Yoan\" Cc: \"JAN Didier\" , \"ddumas\" , \"Gérald LACROIX\" , \"s labourasse\" , \"Stephane roques\" , \"claude trelcat\" , \"benoit gaumet\" , \"jm dao\" , \"a ferrer\" , \"pierre david\" , \"philippe delorme\" , \"Morales Frederic\" , \"CELLI Sylvain\" , \"BOISSONNADE-CORP Pierre\" , \"Richard BOREL\" , \"alain guiraud\" , \"raymond cayzac\" , \"georges pouget\" , \"noel sanchez\" , \"joseph pau\" , \"nicolas teisseire\" , mmohammedi@ardeche.fr , \"jerome gombault\" , \"alain nargeot\" , \"bhilaire\" , \"TEKATLIAN Annick\" , \"CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB\" , \"GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB\" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : \"Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac.\" Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : \" Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment).\" Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien |\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000", - "parametres_llm": { - "agent": "AgentQuestionReponse", - "llm_type": "Mistral", - "parametres": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n", - "temperature": 0.3, - "max_tokens": 2000 - } - } - } - } - ] -} \ No newline at end of file diff --git a/output/ticket_T0150/rapport/ticket_analysis.md b/output/ticket_T0150/rapport/ticket_analysis.md deleted file mode 100644 index e9b8572..0000000 --- a/output/ticket_T0150/rapport/ticket_analysis.md +++ /dev/null @@ -1,69 +0,0 @@ -# Analyse de ticket de support - -## Statistiques - -- Images analysées: 0 (0 pertinentes) -- Questions identifiées: 5 -- Réponses identifiées: 5 - - -## Paramètres LLM par agent - -### Agent de filtrage d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.2 -- **Tokens max**: 500 - -### Agent d'analyse d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.3 -- **Tokens max**: 1024 - -### Agent d'extraction questions-réponses - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - - -## Journal d'actions - -### 2025-04-01 17:30:53 - AgentQuestionReponse (mistral-medium) -**Action**: extract_questions_reponses -**Questions**: 5 -**Réponses**: 5 - - -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| -| **Client**: Notification d'appel Version imprimable Notification d'appel Nous avons reçu un appel pour | **Support**: Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | -| **Client**: Bonjour, Vous pouvez me rappeler? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Etes vous disponible demain dans la matinée ? Je reste | -| **Client**: Oui pas de souci merci. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Mardi 21 Avril 2020 16:57:48 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Etes vous disponible demain dans la matinée ? Je reste | **Support**: Salut Laurent, > Si cela pose un problème, on peut mettre une option dans le logiciel pour imprimer le rapport soit avec la résistance brute, soit avec la résistance corrigée à toi de voir. Cela pose un gros problème cette histoire de résistance brute dans le rapport car M CAZARD obtient une non-conformité du coup. Pouvoir donner la possibilité d'imprimer le PV soit avec la résistance brute, soit avec la résistance corrigée semble être la meilleure solution. En effet, en ce moment il travail sur des ROANE (remblais auto-nivelant non essorable). Il s'agit de béton auto-plaçant et auto-nivelant pour les tranchées de la fibre optique. Cela permet d'intervenir plus facilement en cas de coupure du câble. C'est la raison pour laquelle les valeurs en MPa sont aussi faibles. Ils cherchent des valeurs comprises entre 2 et 3 Mpa. Or avec la résistance brute, il obtient une non conformité. Apparemment ils vont reprendre la travail début mai. Je lui ai dit qu'on allé voir ça pour cette date. C'est faisable ? | -| **Client**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. C'est un peu plus clair. On devrait pouvoir continuer à afficher les deux valeurs de résistances sur les PV et rapports d'essais. Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "Youness BENDEQ" À: "Yohan CAZARD" Envoyé: Lundi 20 Avril 2020 18:40:40 Objet: Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Voir Tâche Bonjour, Je vous contacte suite à votre demande concernant un problème d'impression des PV de compression des éprouvettes 11H22 (résistance brute affichée au lieu de la résistance corrigée). En fait on à eu une non-conformité qui nous a été remonté suite à un audit (qui n'est autre que Mr DIERKENS du CEREMA de Lyon, qui est cité sur la norme 12390-3 et la EN-206/CN) et jusqu'à présent aucun auditeur n'avait fait la remarque. C'est encore une des spécificités incompréhensibles Française. Nous avons 2 normes, la 12390-3 et la EN-206/CN. La norme EN-206/CN indique que pour des écrasements d'éprouvettes 11x22 il faut compenser en retirant 1 MPa. Cependant, lors d'un essai d'écrasement de béton, on doit se limiter strictement aux spécifications de la norme 12390-3 et fournir un PV avec résistance brute. Les corrections de résistance devant être utilisées uniquement pour des calculs statistiques liés à la marque NF BPE (ce qui est d'ailleurs fait dans notre logiciel). Donc c'est très ambigu, car jusqu'à présent on considérait que tous les bétons devaient respecter la norme EN 206/CN, et on disposait d'une case à cocher sur le PV permettant de ne pas appliquer la compensation qu'on aurais du normalement cocher à chaque écrasement... Mais c'est peut explicite, beaucoup d'autres personnes seront, je pense, dans le même cas que vous. Nous avons également posé la question à Mr DIERKENS si nous pouvions rajouter une colonne indiquant la résistance corrigée et la réponse est négative, car on doit se limiter strictement à ce qui est indiqué dans la norme. Je reste | **Support**: Bonjour, Ci-dessous les échanges avec Pierre Paya du Cerema d'Aix, qui s'est mis en relation avec M.Dierkens. Je ne vois pas l'échange complet, pouvez-vous me le renvoyer svp ? Je reste | -| **Client**: Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "PAYA Pierre" À: "CAZARD Yoan" Cc: "JAN Didier" , "ddumas" , "Gérald LACROIX" , "s labourasse" , "Stephane roques" , "claude trelcat" , "benoit gaumet" , "jm dao" , "a ferrer" , "pierre david" , "philippe delorme" , "Morales Frederic" , "CELLI Sylvain" , "BOISSONNADE-CORP Pierre" , "Richard BOREL" , "alain guiraud" , "raymond cayzac" , "georges pouget" , "noel sanchez" , "joseph pau" , "nicolas teisseire" , mmohammedi@ardeche.fr , "jerome gombault" , "alain nargeot" , "bhilaire" , "TEKATLIAN Annick" , "CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB" , "GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : "Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac." Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : " Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment)." Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien | **Support**: -------- Message transféré -------- Sujet : Re: [T0150] Problème d'impression des PV de compression des éprouvettes 11H22 Date : Thu, 23 Apr 2020 14:33:46 +0200 (CEST) De : CAZARD Yoan Pour : support Oui, il faut prendre la valeur corrigée (selon EN 206/CN) pour évaluer la conformité du béton. Il faut bien distinguer : - la norme d'essai 12390-3 qui ne traite que de l'essai et qui n'a pas vocation à se prononcer sur la conformité du matériau : le résultat d'essai est bien la force appliquée (en N) divisée par la section de l'éprouvette, quelle que soit cette section, - la norme béton EN 206/CN, qui donne des indications pour se prononcer sur la conformité du béton, notamment la prise en compte de la taille de éprouvette (11x22) et des corrections à apporter pour se ramener à une éprouvette normalisée 15x30. Le mode de correction des valeurs brutes d'essai est indépendant de la norme d'essai et vous appartient. Dans le cas de carottes prélevées sur ouvrages existant par exemple, la correction ne se fera pas selon l'EN 206/CN mais selon d'autres dispositions. cdlt Pierre PAYA Responsable de l'activité Matériaux, Construction et Certification Laboratoire - service Ouvrages d'Art et Bâtiment - Tél.: 04 42 24 78 30 Direction Territoriale Méditerranée Centre d’études et d’expertise sur les risques, l’environnement, la mobilité et l’aménagement Le 23/04/2020 à 10:36, > CAZARD Yoan (par Internet) a écrit : Bonjour et merci beaucoup Pierre pour ces précisions. C'est plus clair. Donc, on peut bien avoir, comme ce qui se faisait déjà, les deux valeurs affichées sur le rapport d'essais. Par contre, pour la vérification de résistances d'éprouvettes béton, pour des laboratoires départementaux comme nous, et notamment pour de petites valeurs (pour le RAANE par exemple), pouvons-nous continuer à prendre les résistances corrigées pour la conformité du matériau? Yoan CAZARD Responsable Laboratoire Départemental Routes et Ouvrages d'Art Conseil Départemental 66 Direction Infrastructures et Déplacements Développement et Expertise Technique 1265 avenue Julien Panchot 66000 Perpignan 04-68-68-36-90 06-73-87-52-43 De: "PAYA Pierre" À: "CAZARD Yoan" Cc: "JAN Didier" , "ddumas" , "Gérald LACROIX" , "s labourasse" , "Stephane roques" , "claude trelcat" , "benoit gaumet" , "jm dao" , "a ferrer" , "pierre david" , "philippe delorme" , "Morales Frederic" , "CELLI Sylvain" , "BOISSONNADE-CORP Pierre" , "Richard BOREL" , "alain guiraud" , "raymond cayzac" , "georges pouget" , "noel sanchez" , "joseph pau" , "nicolas teisseire" , mmohammedi@ardeche.fr , "jerome gombault" , "alain nargeot" , "bhilaire" , "TEKATLIAN Annick" , "CORDIER Nathalie - CEREMA/DTerMed/LABO AIX/SOAB" , "GONTARD Lisa - CEREMA/DTerMed/LABO AIX/SOAB" Envoyé: Jeudi 23 Avril 2020 10:17:43 Objet: Re: Ecrasements béton bonjour à tous, Je viens d'avoir Michael Dierkens au téléphone et il m'a retransmis la réponse faite à BRG-Lab : "Si une seule valeur apparait, çà ne peut être que la valeur brute (non-corrigée). Une autre solution peut être aussi, s'il y a assez de place sur le rapport d'essai, de faire apparaitre 2 colonnes : 1 corrigée et 1 non-corrigée. Mais la valeur corrigée peut être vue comme une interprétation du résultat et poser des problèmes à un labo qui serait Cofrac." Sa réponse est cohérente (heureusement ...) avec celle que j'avais faite en décembre 2019 : " Je rajoute également que doivent figurer dans le PV d'essais : - la charge maximale à la rupture en N, - la résistance à la compression non corrigée en Mpa (c'est à dire sans prise en compte des corrections de l'EN 206 sur les 11x22 notamment)." Il faut retenir que : - la norme impose d'afficher dans le PV la charge maximale à rupture non corrigée (en N) ainsi que la résistance non corrigée (en MPa) : ces données doivent donc figurer impérativement dans le PV, - la norme n'interdit pas de rajouter des éléments dans le PV, en particulier la résistance corrigée pour des 11x22, si cette donnée vous parait utile (ou pour votre client) : dans ce cas le COFRAC peut considérer que l'affichage de cette valeur corrigée est une interprétation du résultat et demander à vérifier que l'interprétation des résultats a bien été définie clairement dans le contrat qui vous lie à votre client. Pour information nous faisons apparaitre les résistances brutes et corrigées sur nos PV (sous accréditation COFRAC). Je reste disponible pour toute précision. bien | - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 - -**Paramètres LLM utilisés:** -- **model**: mistral-medium -- **temperature**: 0.3 -- **max_tokens**: 2000 -- **top_p**: 1.0 - ---- diff --git a/output/ticket_T0150/ticket_info.json b/output/ticket_T0150/ticket_info.json deleted file mode 100644 index 336960c..0000000 --- a/output/ticket_T0150/ticket_info.json +++ /dev/null @@ -1,164 +0,0 @@ -{ - "id": 162, - "active": true, - "name": "Problème d'impression des PV de compression des éprouvettes 11H22", - "description": "

La résistance affichée sur le PV correspond à la résistance brute au lieu de la résistance corrigée.

", - "sequence": 17, - "stage_id": [ - 8, - "Clôturé" - ], - "kanban_state": "normal", - "create_date": "2020-04-14 12:41:38", - "write_date": "2024-10-03 13:10:50", - "date_start": "2020-04-14 12:41:38", - "date_end": false, - "date_assign": "2020-04-14 12:53:27", - "date_deadline": false, - "date_last_stage_update": "2020-05-06 11:37:10", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 9, - "Youness BENDEQ" - ], - "partner_id": [ - 9043, - "CONSEIL DEPARTEMENTAL DES PYRENEES-ORIENTALES (66), Yoan CAZARD" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "yoan.cazard@cd66.fr", - "email_cc": "", - "working_hours_open": 0.0, - "working_hours_close": 0.0, - "working_days_open": 0.0, - "working_days_close": 0.0, - "website_message_ids": [ - 11323, - 11322, - 11321, - 11320, - 11309, - 11305, - 11304, - 11296, - 11291, - 11183 - ], - "remaining_hours": -0.25, - "effective_hours": 0.25, - "total_hours_spent": 0.25, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [ - 50 - ], - "priority": "0", - "code": "T0150", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 10906, - 10962 - ], - "message_ids": [ - 11463, - 11462, - 11323, - 11322, - 11321, - 11320, - 11310, - 11309, - 11305, - 11304, - 11296, - 11291, - 11279, - 11244, - 11190, - 11189, - 11188, - 11185, - 11184, - 11183, - 11182 - ], - "message_main_attachment_id": false, - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "fc567ae1-ede0-4cd0-b08b-4e3310759d4d", - "create_uid": [ - 1, - "OdooBot" - ], - "write_uid": [ - 1, - "OdooBot" - ], - "x_CBAO_windows_maj_ID": false, - "x_CBAO_version_signalement": false, - "x_CBAO_version_correction": false, - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "youness@cbao.fr", - "attachment_ids": [], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 0, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/162", - "access_warning": "", - "display_name": "[T0150] Problème d'impression des PV de compression des éprouvettes 11H22", - "__last_update": "2024-10-03 13:10:50" -} \ No newline at end of file diff --git a/output/ticket_T0167/attachments/32380_image001.png b/output/ticket_T0167/attachments/32380_image001.png deleted file mode 100644 index 46fee79..0000000 Binary files a/output/ticket_T0167/attachments/32380_image001.png and /dev/null differ diff --git a/output/ticket_T0167/attachments/32382_Problème partie.png b/output/ticket_T0167/attachments/32382_Problème partie.png deleted file mode 100644 index b1e19c3..0000000 Binary files a/output/ticket_T0167/attachments/32382_Problème partie.png and /dev/null differ diff --git a/output/ticket_T0167/attachments_info.json b/output/ticket_T0167/attachments_info.json deleted file mode 100644 index 0d0a2b2..0000000 --- a/output/ticket_T0167/attachments_info.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - { - "id": 32382, - "name": "Problème partie.png", - "mimetype": "image/png", - "create_date": "2020-04-27 06:21:36", - "file_path": "output/ticket_T0167/attachments/32382_Problème partie.png" - }, - { - "id": 32380, - "name": "image001.png", - "mimetype": "image/png", - "create_date": "2020-04-27 06:21:36", - "file_path": "output/ticket_T0167/attachments/32380_image001.png" - } -] \ No newline at end of file diff --git a/output/ticket_T0167/messages.json b/output/ticket_T0167/messages.json deleted file mode 100644 index 59672fe..0000000 --- a/output/ticket_T0167/messages.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Pb d'affaire/chantier/partie dans un programme d'essai", - "code": "T0167", - "description": "

Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ).

En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE.

J’ai essayé de modifié la partie mais je n’y arrive pas.

", - "date_create": "2020-04-27 06:21:36", - "role": "system", - "type": "contexte", - "body": "TICKET T0167: Pb d'affaire/chantier/partie dans un programme d'essai.\n\nDESCRIPTION:

Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ).

En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE.

J’ai essayé de modifié la partie mais je n’y arrive pas.

" - }, - { - "id": "11333", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-27 06:20:22", - "email_from": "Youness BENDEQ ", - "subject": "Pb d'affaire/chantier/partie dans un programme d'essai", - "body": "-------- Message transféré -------- Sujet : De retour ! Date : Mon, 20 Apr 2020 14:52:05 +0000 De : LENEVEU Guillaume Pour : Youness BENDEQ Bonjour Youness, J’espère que tu vas bien ainsi que toute l’équipe BRG-LAB. Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas. Merci de ta réponse. Bonne fin de journée." - } -] \ No newline at end of file diff --git a/output/ticket_T0167/messages.json.backup b/output/ticket_T0167/messages.json.backup deleted file mode 100644 index c9295dc..0000000 --- a/output/ticket_T0167/messages.json.backup +++ /dev/null @@ -1,125 +0,0 @@ -[ - { - "id": 11346, - "body": "", - "date": "2020-04-27 07:24:40", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11332, - "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai" - ] - }, - { - "id": 11345, - "body": "", - "date": "2020-04-27 07:20:20", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11332, - "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai" - ] - }, - { - "id": 11344, - "body": "", - "date": "2020-04-27 07:19:57", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11332, - "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai" - ] - }, - { - "id": 11343, - "body": "", - "date": "2020-04-27 07:19:29", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": [ - 11332, - "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai" - ] - }, - { - "id": 11342, - "body": "", - "date": "2020-04-27 07:15:48", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "\"Youness BENDEQ\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11335, - "body": "", - "date": "2020-04-27 06:21:37", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11334, - "body": "", - "date": "2020-04-27 06:21:37", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - }, - { - "id": 11333, - "body": "
\r\n
\r\n

\r\n
\r\n -------- Message transféré --------\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
Sujet :\r\n De retour !
Date : Mon, 20 Apr 2020 14:52:05 +0000
De : LENEVEU Guillaume <Guillaume.LENEVEU@calvados.fr>
Pour : Youness BENDEQ <youness.bendeq@cbao.fr>
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n

Bonjour Youness,

\r\n

 

\r\n

J’espère que tu vas bien ainsi que toute\r\n l’équipe BRG-LAB.

\r\n

 

\r\n

Je viens vers toi car Mr NOVO m’a fait\r\n remonter un léger beug sur le numéro d’échantillon B2020-0001\r\n (Voir PJ). En effet, il n’arrive pas à mettre le nom de la\r\n partie dans la partie ( en rouge sur la PJ). Il faudrait\r\n mettre « joint de chaussée côté giberville » comme stipulé\r\n dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403\r\n – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE\r\n GIBERVILLE.

\r\n

 

\r\n

J’ai essayé de modifié la partie mais je\r\n n’y arrive pas.

\r\n

 

\r\n

Merci de ta réponse.

\r\n

Bonne fin de journée.

\r\n

Cordialement,

\r\n

Guillaume\r\n LENEVEU

\r\n

DGA\r\n Aménagement et Environnement
\r\n Direction de l’eau et des Risques
\r\n Adjoint au Chef du service Laboratoire Routes et Matériaux
\r\n 24 rue des Monts Panneaux ZI Ouest

\r\n

14650\r\n Carpiquet

\r\n

Tél.\r\n 02 31 26 52 62

\r\n

Port.\r\n 06 11 39 10 60

\r\n

Fax.\r\n 02\r\n 31 26 99 38
\r\n Mail.
guillaume.leneveu@calvados.fr

\r\n

\r\n

 

\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n
**************************************************************************************************\r\n« Cette transmission contient des informations confidentielles et/ou personnelles\r\nappartenant au conseil départemental du Calvados pour être utilisées exclusivement par le\r\ndestinataire. Toute utilisation, reproduction, publication, diffusion en l'état ou\r\npartiellement par une autre personne que le destinataire est interdite, sauf autorisation\r\nexpresse du conseil départemental du Calvados. En cas d'erreur de transmission, merci de\r\ndétruire le(s) document(s) reçu(s). Le conseil départemental du Calvados n'est pas\r\nresponsable des virus, altérations, falsifications.\r\nDroits réservés - conseil départemental du Calvados».\r\n**************************************************************************************************
\r\n
\r\n
\r\n
\r\n", - "date": "2020-04-27 06:20:22", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "email_from": "Youness BENDEQ ", - "subject": "Pb d'affaire/chantier/partie dans un programme d'essai", - "parent_id": [ - 11332, - "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai" - ] - }, - { - "id": 11332, - "body": "", - "date": "2020-04-27 06:21:37", - "author_id": [ - 2, - "OdooBot" - ], - "email_from": "\"OdooBot\" ", - "subject": false, - "parent_id": false - } -] \ No newline at end of file diff --git a/output/ticket_T0167/questions_reponses.md b/output/ticket_T0167/questions_reponses.md deleted file mode 100644 index 1774812..0000000 --- a/output/ticket_T0167/questions_reponses.md +++ /dev/null @@ -1,15 +0,0 @@ -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 \ No newline at end of file diff --git a/output/ticket_T0167/rapport/ticket_analysis.json b/output/ticket_T0167/rapport/ticket_analysis.json deleted file mode 100644 index 66ddc3d..0000000 --- a/output/ticket_T0167/rapport/ticket_analysis.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "entries": [ - { - "timestamp": "2025-04-01 17:34:01", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T0167/attachments/32382_Problème partie.png", - "response": { - "pertinente": false, - "type_image": "autre", - "description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.", - "confiance": 90, - "justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:34:01", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T0167/attachments/32380_image001.png", - "response": { - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:34:01", - "action": "analyze_image", - "agent": "AgentAnalyseImage", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.3, - "max_tokens": 1024, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n" - }, - "image_path": "output/ticket_T0167/attachments/32380_image001.png", - "response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}", - "tokens": { - "prompt_tokens": 208, - "completion_tokens": 150, - "total_tokens": 358 - } - }, - { - "timestamp": "2025-04-01 17:34:01", - "action": "extract_questions_reponses", - "agent": "AgentQuestionReponse", - "llm": { - "model": "mistral-medium" - }, - "parametres_llm": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "response": { - "success": true, - "messages_analyses": [ - { - "id": 11333, - "date": "2020-04-27 06:20:22", - "role": "Support", - "type": "Information", - "contenu": "-------- Message transféré -------- Sujet : De retour ! Date : Mon, 20 Apr 2020 14:52:05 +0000 De : LENEVEU Guillaume Pour : Youness BENDEQ Bonjour Youness, J’espère que tu vas bien ainsi que toute l’équipe BRG-LAB. Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrai" - } - ], - "paires_qr": [], - "nb_questions": 0, - "nb_reponses": 0, - "tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000", - "parametres_llm": { - "agent": "AgentQuestionReponse", - "llm_type": "Mistral", - "parametres": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n", - "temperature": 0.3, - "max_tokens": 2000 - } - } - } - } - ] -} \ No newline at end of file diff --git a/output/ticket_T0167/rapport/ticket_analysis.md b/output/ticket_T0167/rapport/ticket_analysis.md deleted file mode 100644 index 5c744ce..0000000 --- a/output/ticket_T0167/rapport/ticket_analysis.md +++ /dev/null @@ -1,116 +0,0 @@ -# Analyse de ticket de support - -## Statistiques - -- Images analysées: 2 (1 pertinentes) -- Questions identifiées: 0 -- Réponses identifiées: 0 - - -## Paramètres LLM par agent - -### Agent de filtrage d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.2 -- **Tokens max**: 500 - -### Agent d'analyse d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.3 -- **Tokens max**: 1024 - -### Agent d'extraction questions-réponses - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - - -## Journal d'actions - -### 2025-04-01 17:34:01 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 32382_Problème partie.png -**Résultat**: Non pertinente -**Type**: autre -**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:34:01 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 32380_image001.png -**Résultat**: Pertinente -**Type**: capture_ecran -**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:34:01 - AgentAnalyseImage (pixtral-12b-2409) -**Action**: analyze_image -**Image analysée**: 32380_image001.png - -**Analyse**: -``` -{ - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket." -} -``` - -**Paramètres LLM utilisés:** -- **model**: pixtral-12b-2409 -- **temperature**: 0.3 -- **max_tokens**: 1024 -- **top_p**: 1.0 - -**Tokens utilisés**: -- Prompt: 208 -- Completion: 150 -- Total: 358 - ---- - -### 2025-04-01 17:34:01 - AgentQuestionReponse (mistral-medium) -**Action**: extract_questions_reponses -**Questions**: 0 -**Réponses**: 0 - - -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 - -**Paramètres LLM utilisés:** -- **model**: mistral-medium -- **temperature**: 0.3 -- **max_tokens**: 2000 -- **top_p**: 1.0 - ---- diff --git a/output/ticket_T0167/ticket_info.json b/output/ticket_T0167/ticket_info.json deleted file mode 100644 index b41a537..0000000 --- a/output/ticket_T0167/ticket_info.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "id": 179, - "active": true, - "name": "Pb d'affaire/chantier/partie dans un programme d'essai", - "description": "

Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ).

En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE.

J’ai essayé de modifié la partie mais je n’y arrive pas.

", - "sequence": 22, - "stage_id": [ - 8, - "Clôturé" - ], - "kanban_state": "normal", - "create_date": "2020-04-27 06:21:36", - "write_date": "2024-10-03 13:10:50", - "date_start": "2020-04-20 14:52:00", - "date_end": false, - "date_assign": "2020-04-27 07:15:48", - "date_deadline": false, - "date_last_stage_update": "2020-04-27 07:24:40", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 9, - "Youness BENDEQ" - ], - "partner_id": [ - 8504, - "CONSEIL DEPARTEMENTAL DU CALVADOS (14), Guillaume LENEVEU" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "guillaume.leneveu@calvados.fr", - "email_cc": "", - "working_hours_open": 0.0, - "working_hours_close": 0.0, - "working_days_open": 0.0, - "working_days_close": 0.0, - "website_message_ids": [ - 11333 - ], - "remaining_hours": -0.5, - "effective_hours": 0.5, - "total_hours_spent": 0.5, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [ - 51 - ], - "priority": "0", - "code": "T0167", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 10972 - ], - "message_ids": [ - 11346, - 11345, - 11344, - 11343, - 11342, - 11335, - 11334, - 11333, - 11332 - ], - "message_main_attachment_id": [ - 32380, - "image001.png" - ], - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "cd4fbf5c-27d3-48ed-8c9b-c07f20c3e2d4", - "create_uid": [ - 1, - "OdooBot" - ], - "write_uid": [ - 1, - "OdooBot" - ], - "x_CBAO_windows_maj_ID": false, - "x_CBAO_version_signalement": false, - "x_CBAO_version_correction": false, - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "youness@cbao.fr", - "attachment_ids": [], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 2, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/179", - "access_warning": "", - "display_name": "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai", - "__last_update": "2024-10-03 13:10:50" -} \ No newline at end of file diff --git a/output/ticket_T11067/attachments/144792_image003.png b/output/ticket_T11067/attachments/144792_image003.png deleted file mode 100644 index 121eaf8..0000000 Binary files a/output/ticket_T11067/attachments/144792_image003.png and /dev/null differ diff --git a/output/ticket_T11067/attachments/144794_image004.jpg b/output/ticket_T11067/attachments/144794_image004.jpg deleted file mode 100644 index 5c3401d..0000000 Binary files a/output/ticket_T11067/attachments/144794_image004.jpg and /dev/null differ diff --git a/output/ticket_T11067/attachments/144796_image.png b/output/ticket_T11067/attachments/144796_image.png deleted file mode 100644 index db688c6..0000000 Binary files a/output/ticket_T11067/attachments/144796_image.png and /dev/null differ diff --git a/output/ticket_T11067/attachments_info.json b/output/ticket_T11067/attachments_info.json deleted file mode 100644 index 249e22c..0000000 --- a/output/ticket_T11067/attachments_info.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "id": 144796, - "name": "image.png", - "mimetype": "image/png", - "create_date": "2025-03-18 14:18:51", - "file_path": "output/ticket_T11067/attachments/144796_image.png" - }, - { - "id": 144794, - "name": "image004.jpg", - "mimetype": "image/jpeg", - "create_date": "2025-03-18 13:22:27", - "file_path": "output/ticket_T11067/attachments/144794_image004.jpg" - }, - { - "id": 144792, - "name": "image003.png", - "mimetype": "image/png", - "create_date": "2025-03-18 13:22:27", - "file_path": "output/ticket_T11067/attachments/144792_image003.png" - } -] \ No newline at end of file diff --git a/output/ticket_T11067/messages.json b/output/ticket_T11067/messages.json deleted file mode 100644 index 8b6a378..0000000 --- a/output/ticket_T11067/messages.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "changement nom centrale d'enrobage", - "code": "T11067", - "description": "


", - "date_create": "2025-03-18 13:22:27", - "role": "system", - "type": "contexte", - "body": "TICKET T11067: changement nom centrale d'enrobage.\n\nDESCRIPTION:


" - }, - { - "id": 227725, - "author_id": [ - 5144, - "CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL" - ], - "role": "Client", - "type": "Question", - "date": "2025-03-18 13:18:31", - "email_from": "CARVAL Dominique ", - "subject": "changement nom centrale d'enrobage", - "body": "Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)" - }, - { - "id": 227731, - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "role": "Support", - "type": "Réponse", - "date": "2025-03-18 14:18:51", - "email_from": "support@cbao.fr", - "subject": "Re: [T11067] - changement nom centrale d'enrobage", - "body": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire." - } -] \ No newline at end of file diff --git a/output/ticket_T11067/messages.json.backup b/output/ticket_T11067/messages.json.backup deleted file mode 100644 index cffc801..0000000 --- a/output/ticket_T11067/messages.json.backup +++ /dev/null @@ -1,35 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "changement nom centrale d'enrobage", - "code": "T11067", - "description": "


", - "date_create": "2025-03-18 13:22:27" - }, - { - "id": 227725, - "author_id": [ - 5144, - "CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL" - ], - "role": "Client", - "type": "Question", - "date": "2025-03-18 13:18:31", - "email_from": "CARVAL Dominique ", - "subject": "changement nom centrale d'enrobage", - "body": "Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)" - }, - { - "id": 227731, - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "role": "Support", - "type": "Réponse", - "date": "2025-03-18 14:18:51", - "email_from": "support@cbao.fr", - "subject": "Re: [T11067] - changement nom centrale d'enrobage", - "body": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire." - } -] \ No newline at end of file diff --git a/output/ticket_T11067/questions_reponses.md b/output/ticket_T11067/questions_reponses.md deleted file mode 100644 index 7f56260..0000000 --- a/output/ticket_T11067/questions_reponses.md +++ /dev/null @@ -1,16 +0,0 @@ -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| -| **Client**: Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. | - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 \ No newline at end of file diff --git a/output/ticket_T11067/rapport/ticket_analysis.json b/output/ticket_T11067/rapport/ticket_analysis.json deleted file mode 100644 index 5ebf9ee..0000000 --- a/output/ticket_T11067/rapport/ticket_analysis.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "entries": [ - { - "timestamp": "2025-04-01 17:08:06", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T11067/attachments/144796_image.png", - "response": { - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:08:06", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T11067/attachments/144794_image004.jpg", - "response": { - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:08:06", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T11067/attachments/144792_image003.png", - "response": { - "pertinente": false, - "type_image": "logo", - "description": "Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique.", - "confiance": 90, - "justification": "L'image ne contient pas d'éléments techniques utiles pour résoudre un problème.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:08:06", - "action": "analyze_image", - "agent": "AgentAnalyseImage", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.3, - "max_tokens": 1024, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n" - }, - "image_path": "output/ticket_T11067/attachments/144796_image.png", - "response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}", - "tokens": { - "prompt_tokens": 84, - "completion_tokens": 150, - "total_tokens": 234 - } - }, - { - "timestamp": "2025-04-01 17:08:06", - "action": "analyze_image", - "agent": "AgentAnalyseImage", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.3, - "max_tokens": 1024, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n" - }, - "image_path": "output/ticket_T11067/attachments/144794_image004.jpg", - "response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}", - "tokens": { - "prompt_tokens": 84, - "completion_tokens": 150, - "total_tokens": 234 - } - }, - { - "timestamp": "2025-04-01 17:08:06", - "action": "extract_questions_reponses", - "agent": "AgentQuestionReponse", - "llm": { - "model": "mistral-medium" - }, - "parametres_llm": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "response": { - "success": true, - "messages_analyses": [ - { - "id": "ticket_info", - "date": "", - "role": "Client", - "type": "Question", - "contenu": "TICKET T11067: changement nom centrale d'enrobage. DESCRIPTION:" - }, - { - "id": 227725, - "date": "2025-03-18 13:18:31", - "role": "Client", - "type": "Question", - "contenu": "Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)" - }, - { - "id": 227731, - "date": "2025-03-18 14:18:51", - "role": "Support", - "type": "Information", - "contenu": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire." - } - ], - "paires_qr": [ - { - "numero": "1", - "question": { - "role": "Client", - "contenu": "Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites)" - }, - "reponse": { - "role": "Support", - "contenu": "Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire." - } - } - ], - "nb_questions": 1, - "nb_reponses": 1, - "tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n| **Client**: Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. |\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000", - "parametres_llm": { - "agent": "AgentQuestionReponse", - "llm_type": "Mistral", - "parametres": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n", - "temperature": 0.3, - "max_tokens": 2000 - } - } - } - } - ] -} \ No newline at end of file diff --git a/output/ticket_T11067/rapport/ticket_analysis.md b/output/ticket_T11067/rapport/ticket_analysis.md deleted file mode 100644 index 8942a15..0000000 --- a/output/ticket_T11067/rapport/ticket_analysis.md +++ /dev/null @@ -1,157 +0,0 @@ -# Analyse de ticket de support - -## Statistiques - -- Images analysées: 3 (2 pertinentes) -- Questions identifiées: 1 -- Réponses identifiées: 1 - - -## Paramètres LLM par agent - -### Agent de filtrage d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.2 -- **Tokens max**: 500 - -### Agent d'analyse d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.3 -- **Tokens max**: 1024 - -### Agent d'extraction questions-réponses - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - - -## Journal d'actions - -### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 144796_image.png -**Résultat**: Pertinente -**Type**: capture_ecran -**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 144794_image004.jpg -**Résultat**: Pertinente -**Type**: capture_ecran -**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:08:06 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 144792_image003.png -**Résultat**: Non pertinente -**Type**: logo -**Description**: Cette image semble être un logo, une signature ou un élément graphique décoratif, et n'est pas pertinente dans un contexte technique. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:08:06 - AgentAnalyseImage (pixtral-12b-2409) -**Action**: analyze_image -**Image analysée**: 144796_image.png - -**Analyse**: -``` -{ - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket." -} -``` - -**Paramètres LLM utilisés:** -- **model**: pixtral-12b-2409 -- **temperature**: 0.3 -- **max_tokens**: 1024 -- **top_p**: 1.0 - -**Tokens utilisés**: -- Prompt: 84 -- Completion: 150 -- Total: 234 - ---- - -### 2025-04-01 17:08:06 - AgentAnalyseImage (pixtral-12b-2409) -**Action**: analyze_image -**Image analysée**: 144794_image004.jpg - -**Analyse**: -``` -{ - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket." -} -``` - -**Paramètres LLM utilisés:** -- **model**: pixtral-12b-2409 -- **temperature**: 0.3 -- **max_tokens**: 1024 -- **top_p**: 1.0 - -**Tokens utilisés**: -- Prompt: 84 -- Completion: 150 -- Total: 234 - ---- - -### 2025-04-01 17:08:06 - AgentQuestionReponse (mistral-medium) -**Action**: extract_questions_reponses -**Questions**: 1 -**Réponses**: 1 - - -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| -| **Client**: Bonjour, 3 centrales d’enrobage ont changé de nom. Comment faire ce changement sur BRG-LAB ? (ici ARMOR ENROBÉS devient BREIZH ENROBÉS sur 3 sites) | **Support**: Bonjour , Effectivement, il y a une anomalie lors du changement du nom d'un poste de production. Les mises à jour déployées ce soir et demain devraient vous permettre d’effectuer cette modification. Pour cela, il faut éditer le nom du poste de production d’enrobée, l’enregistrer dans la fiche générale, puis cliquer sur la petite flèche à droite du nom et le modifier. Je reste à votre entière disposition pour toute information complémentaire. | - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 - -**Paramètres LLM utilisés:** -- **model**: mistral-medium -- **temperature**: 0.3 -- **max_tokens**: 2000 -- **top_p**: 1.0 - ---- diff --git a/output/ticket_T11067/ticket_info.json b/output/ticket_T11067/ticket_info.json deleted file mode 100644 index 50f63b1..0000000 --- a/output/ticket_T11067/ticket_info.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "id": 11046, - "active": true, - "name": "changement nom centrale d'enrobage", - "description": "


", - "sequence": 10, - "stage_id": [ - 32, - "En attente d'infos / retours" - ], - "kanban_state": "normal", - "create_date": "2025-03-18 13:22:27", - "write_date": "2025-03-18 14:19:28", - "date_start": "2025-03-18 13:22:28", - "date_end": false, - "date_assign": "2025-03-18 13:42:04", - "date_deadline": "2025-04-02", - "date_last_stage_update": "2025-03-18 14:19:28", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 32, - "Romuald GRUSON" - ], - "partner_id": [ - 5144, - "CONSEIL DEPARTEMENTAL DU MORBIHAN (56), Dominique CARVAL" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "CARVAL Dominique ", - "email_cc": "", - "working_hours_open": 0.0, - "working_hours_close": 0.0, - "working_days_open": 0.0, - "working_days_close": 0.0, - "website_message_ids": [ - 227731, - 227725 - ], - "remaining_hours": 0.0, - "effective_hours": 0.0, - "total_hours_spent": 0.0, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [], - "priority": "0", - "code": "T11067", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 89590, - 89592, - 89593 - ], - "message_ids": [ - 227733, - 227732, - 227731, - 227730, - 227728, - 227726, - 227725, - 227724 - ], - "message_main_attachment_id": [ - 144792, - "image003.png" - ], - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "3295983b-a3aa-4d8c-817d-2332829ca264", - "create_uid": [ - 1, - "OdooBot" - ], - "write_uid": [ - 32, - "Romuald GRUSON" - ], - "x_CBAO_windows_maj_ID": false, - "x_CBAO_version_signalement": false, - "x_CBAO_version_correction": false, - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "romuald@mail.cbao.fr", - "attachment_ids": [], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 3, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/11046", - "access_warning": "", - "display_name": "[T11067] changement nom centrale d'enrobage", - "__last_update": "2025-03-18 14:19:28" -} \ No newline at end of file diff --git a/output/ticket_T11094/attachments/144918_image.png b/output/ticket_T11094/attachments/144918_image.png deleted file mode 100644 index e3fdf37..0000000 Binary files a/output/ticket_T11094/attachments/144918_image.png and /dev/null differ diff --git a/output/ticket_T11094/attachments_info.json b/output/ticket_T11094/attachments_info.json deleted file mode 100644 index 74819b5..0000000 --- a/output/ticket_T11094/attachments_info.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "id": 144918, - "name": "image.png", - "mimetype": "image/png", - "create_date": "2025-03-21 14:50:36", - "file_path": "output/ticket_T11094/attachments/144918_image.png" - } -] \ No newline at end of file diff --git a/output/ticket_T11094/messages.json b/output/ticket_T11094/messages.json deleted file mode 100644 index 06d71e6..0000000 --- a/output/ticket_T11094/messages.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Calcul de la masse volumique", - "code": "T11094", - "description": "

Point particulier :

  • Essais :E2025-0002
  • Une norme :NF EN 12697-6
  • Multi laboratoire :Rouen labo sol
  • Le cas est bloquant

Description du problème :

Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement

", - "date_create": "2025-03-21 07:54:57", - "role": "system", - "type": "contexte", - "body": "TICKET T11094: Calcul de la masse volumique.\n\nDESCRIPTION:

Point particulier :

  • Essais :E2025-0002
  • Une norme :NF EN 12697-6
  • Multi laboratoire :Rouen labo sol
  • Le cas est bloquant

Description du problème :

Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement

" - }, - { - "id": "228106", - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "role": "Support", - "type": "Réponse", - "date": "2025-03-21 14:50:37", - "email_from": "support@cbao.fr", - "subject": "Re: [T11094] - Calcul de la masse volumique", - "body": "Bonjour , Pour obtenir une valeur, il est nécessaire de renseigner la \"masse volumique du produit d'étanchéité\". Vous trouverez une capture d'écran ci-dessous pour vous guider dans cette démarche. Je reste à votre entière disposition pour toute information complémentaire." - } -] \ No newline at end of file diff --git a/output/ticket_T11094/messages.json.backup b/output/ticket_T11094/messages.json.backup deleted file mode 100644 index f5bdefd..0000000 --- a/output/ticket_T11094/messages.json.backup +++ /dev/null @@ -1,83 +0,0 @@ -[ - { - "id": 228108, - "body": "", - "date": "2025-03-21 14:50:41", - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "email_from": "\"Romuald GRUSON\" ", - "subject": false, - "parent_id": [ - 228083, - "[T11094] Calcul de la masse volumique" - ] - }, - { - "id": 228107, - "body": "", - "date": "2025-03-21 14:50:41", - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "email_from": "\"Romuald GRUSON\" ", - "subject": false, - "parent_id": false - }, - { - "id": 228106, - "body": "

Bonjour,

Pour obtenir une valeur, il est nécessaire de renseigner la \"masse volumique du produit d'étanchéité\". Vous trouverez une capture d'écran ci-dessous pour vous guider dans cette démarche.

\"image.png\"

Je reste à votre entière disposition pour toute information complémentaire.

Cordialement,

---

Support technique
 

\n

\"CBAO

\n

Afin d'assurer une meilleure traçabilité et vous garantir une prise en charge optimale, nous vous invitons à envoyer vos demandes d'assistance technique à support@cbao.fr
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.

Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.

", - "date": "2025-03-21 14:50:37", - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "email_from": "support@cbao.fr", - "subject": "Re: [T11094] - Calcul de la masse volumique", - "parent_id": [ - 228083, - "[T11094] Calcul de la masse volumique" - ] - }, - { - "id": 228091, - "body": "", - "date": "2025-03-21 10:57:58", - "author_id": [ - 32165, - "Romuald GRUSON" - ], - "email_from": "\"Romuald GRUSON\" ", - "subject": false, - "parent_id": false - }, - { - "id": 228086, - "body": "", - "date": "2025-03-21 08:20:00", - "author_id": [ - 28961, - "Fabien LAFAY" - ], - "email_from": "\"Fabien LAFAY\" ", - "subject": false, - "parent_id": [ - 228083, - "[T11094] Calcul de la masse volumique" - ] - }, - { - "id": 228083, - "body": "", - "date": "2025-03-21 07:54:57", - "author_id": [ - 30810, - "Support Robot" - ], - "email_from": "\"Support Robot\" ", - "subject": false, - "parent_id": false - } -] \ No newline at end of file diff --git a/output/ticket_T11094/questions_reponses.md b/output/ticket_T11094/questions_reponses.md deleted file mode 100644 index 1774812..0000000 --- a/output/ticket_T11094/questions_reponses.md +++ /dev/null @@ -1,15 +0,0 @@ -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 \ No newline at end of file diff --git a/output/ticket_T11094/rapport/ticket_analysis.json b/output/ticket_T11094/rapport/ticket_analysis.json deleted file mode 100644 index ea72403..0000000 --- a/output/ticket_T11094/rapport/ticket_analysis.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "entries": [ - { - "timestamp": "2025-04-01 17:18:01", - "action": "filter_image", - "agent": "AgentFiltreImages", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "image_path": "output/ticket_T11094/attachments/144918_image.png", - "response": { - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalités techniques liées au problème décrit dans le ticket.", - "parametres_llm": { - "agent": "AgentFiltreImages", - "llm_type": "Pixtral", - "parametres": { - "model": "pixtral-12b-2409", - "temperature": 0.2, - "max_tokens": 500, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse d'images techniques. Votre mission est de déterminer\nsi une image est pertinente dans un contexte de support technique ou non.\n\nImages PERTINENTES:\n- Captures d'écran montrant des problèmes, erreurs, bugs\n- Photos d'équipements avec problèmes visibles\n- Schémas techniques ou diagrammes\n- Graphiques de données techniques\n\nImages NON PERTINENTES:\n- Logos d'entreprise\n- Signatures ou avatars\n- Icônes ou boutons isolés\n- Bannières décoratives, séparateurs\n- Images génériques sans information technique\n", - "temperature": 0.2, - "max_tokens": 500 - } - } - } - }, - { - "timestamp": "2025-04-01 17:18:01", - "action": "analyze_image", - "agent": "AgentAnalyseImage", - "llm": { - "model": "pixtral-12b-2409" - }, - "parametres_llm": { - "model": "pixtral-12b-2409", - "temperature": 0.3, - "max_tokens": 1024, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse d'images techniques.\nVotre rôle est d'examiner des captures d'écran ou des photos liées à des problèmes techniques et de:\n1. Décrire précisément le contenu de l'image\n2. Identifier les éléments techniques visibles (erreurs, interfaces, configurations)\n3. Extraire tout texte visible (messages d'erreur, logs, indicateurs)\n\nSoyez précis et factuel dans votre analyse.\n" - }, - "image_path": "output/ticket_T11094/attachments/144918_image.png", - "response": "{\n \"pertinente\": true,\n \"type_image\": \"capture_ecran\",\n \"description\": \"Capture d'\\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.\",\n \"confiance\": 85,\n \"justification\": \"L'image montre clairement une interface utilisateur avec des fonctionnalit\\u00e9s techniques li\\u00e9es au probl\\u00e8me d\\u00e9crit dans le ticket.\"\n}", - "tokens": { - "prompt_tokens": 162, - "completion_tokens": 150, - "total_tokens": 312 - } - }, - { - "timestamp": "2025-04-01 17:18:01", - "action": "extract_questions_reponses", - "agent": "AgentQuestionReponse", - "llm": { - "model": "mistral-medium" - }, - "parametres_llm": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "response": { - "success": true, - "messages_analyses": [ - { - "id": 228106, - "date": "2025-03-21 14:50:37", - "role": "Support", - "type": "Information", - "contenu": "Bonjour , Pour obtenir une valeur, il est nécessaire de renseigner la \"masse volumique du produit d'étanchéité\". Vous trouverez une capture d'écran ci-dessous pour vous guider dans cette démarche. Je reste à votre entière disposition pour toute information complémentaire." - } - ], - "paires_qr": [], - "nb_questions": 0, - "nb_reponses": 0, - "tableau_md": "# Analyse des Questions et Réponses\n\n| Question | Réponse |\n|---------|---------|\n\n## Paramètres LLM utilisés\n\n- **Type de LLM**: Mistral\n- **Modèle**: mistral-medium\n- **Température**: 0.3\n- **Tokens max**: 2000\n\n**Paramètres modifiés durant l'analyse:**\n- **temperature**: 0.3\n- **max_tokens**: 2000", - "parametres_llm": { - "agent": "AgentQuestionReponse", - "llm_type": "Mistral", - "parametres": { - "model": "mistral-medium", - "temperature": 0.3, - "max_tokens": 2000, - "top_p": 1.0, - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n" - }, - "parametres_modifies": { - "system_prompt": "\nVous êtes un expert en analyse de conversations de support technique.\n\nVotre mission est d'identifier avec précision:\n1. Le rôle de chaque intervenant (client ou support technique)\n2. La nature de chaque message (question, réponse, information additionnelle)\n3. Le contenu essentiel de chaque message en éliminant les formules de politesse,\n signatures, mentions légales et autres éléments non pertinents\n\nPour l'identification client/support:\n- Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email\n comme @cbao.fr, @odoo.com, mentions \"support technique\", etc.\n- Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions\n\nPour la classification en question/réponse:\n- Questions: Demandes explicites (avec \"?\"), demandes implicites de résolution\n de problèmes, descriptions de bugs ou dysfonctionnements\n- Réponses: Explications techniques, solutions proposées, instructions fournies\n par le support\n\nConcentrez-vous uniquement sur le contenu technique utile en ignorant tous les\néléments superflus qui n'apportent pas d'information sur le problème ou sa solution.\n", - "temperature": 0.3, - "max_tokens": 2000 - } - } - } - } - ] -} \ No newline at end of file diff --git a/output/ticket_T11094/rapport/ticket_analysis.md b/output/ticket_T11094/rapport/ticket_analysis.md deleted file mode 100644 index f4ecff6..0000000 --- a/output/ticket_T11094/rapport/ticket_analysis.md +++ /dev/null @@ -1,104 +0,0 @@ -# Analyse de ticket de support - -## Statistiques - -- Images analysées: 1 (1 pertinentes) -- Questions identifiées: 0 -- Réponses identifiées: 0 - - -## Paramètres LLM par agent - -### Agent de filtrage d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.2 -- **Tokens max**: 500 - -### Agent d'analyse d'images - -- **Type de LLM**: Pixtral -- **Modèle**: pixtral-12b-2409 -- **Température**: 0.3 -- **Tokens max**: 1024 - -### Agent d'extraction questions-réponses - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - - -## Journal d'actions - -### 2025-04-01 17:18:01 - AgentFiltreImages (pixtral-12b-2409) -**Action**: filter_image -**Image**: 144918_image.png -**Résultat**: Pertinente -**Type**: capture_ecran -**Description**: Capture d'écran d'une interface montrant le formulaire de modification d'une centrale d'enrobage. - -**Paramètres LLM utilisés:** -- Température: 0.2 - ---- - -### 2025-04-01 17:18:01 - AgentAnalyseImage (pixtral-12b-2409) -**Action**: analyze_image -**Image analysée**: 144918_image.png - -**Analyse**: -``` -{ - "pertinente": true, - "type_image": "capture_ecran", - "description": "Capture d'\u00e9cran d'une interface montrant le formulaire de modification d'une centrale d'enrobage.", - "confiance": 85, - "justification": "L'image montre clairement une interface utilisateur avec des fonctionnalit\u00e9s techniques li\u00e9es au probl\u00e8me d\u00e9crit dans le ticket." -} -``` - -**Paramètres LLM utilisés:** -- **model**: pixtral-12b-2409 -- **temperature**: 0.3 -- **max_tokens**: 1024 -- **top_p**: 1.0 - -**Tokens utilisés**: -- Prompt: 162 -- Completion: 150 -- Total: 312 - ---- - -### 2025-04-01 17:18:01 - AgentQuestionReponse (mistral-medium) -**Action**: extract_questions_reponses -**Questions**: 0 -**Réponses**: 0 - - -# Analyse des Questions et Réponses - -| Question | Réponse | -|---------|---------| - -## Paramètres LLM utilisés - -- **Type de LLM**: Mistral -- **Modèle**: mistral-medium -- **Température**: 0.3 -- **Tokens max**: 2000 - -**Paramètres modifiés durant l'analyse:** -- **temperature**: 0.3 -- **max_tokens**: 2000 - -**Paramètres LLM utilisés:** -- **model**: mistral-medium -- **temperature**: 0.3 -- **max_tokens**: 2000 -- **top_p**: 1.0 - ---- diff --git a/output/ticket_T11094/ticket_info.json b/output/ticket_T11094/ticket_info.json deleted file mode 100644 index 546ac82..0000000 --- a/output/ticket_T11094/ticket_info.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "id": 11073, - "active": true, - "name": "Calcul de la masse volumique", - "description": "

Point particulier :

  • Essais :E2025-0002
  • Une norme :NF EN 12697-6
  • Multi laboratoire :Rouen labo sol
  • Le cas est bloquant

Description du problème :

Bonjour,\r\nEst-il possible de vérifier pourquoi le calcul de ma masse volumique ne fonctionne pas.\r\n\r\nCordialement

", - "sequence": 10, - "stage_id": [ - 32, - "En attente d'infos / retours" - ], - "kanban_state": "normal", - "create_date": "2025-03-21 07:54:57", - "write_date": "2025-03-21 14:50:41", - "date_start": "2025-03-21 07:54:57", - "date_end": false, - "date_assign": "2025-03-21 10:57:58", - "date_deadline": "2025-04-05", - "date_last_stage_update": "2025-03-21 14:50:41", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 32, - "Romuald GRUSON" - ], - "partner_id": [ - 29954, - "INFRANEO - PANTIN, Mickaël RESSE" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "m.resse@infraneo.com", - "email_cc": "", - "working_hours_open": 1.9661111111111111, - "working_hours_close": 0.0, - "working_days_open": 0.0819212962962963, - "working_days_close": 0.0, - "website_message_ids": [ - 228106 - ], - "remaining_hours": 0.0, - "effective_hours": 0.0, - "total_hours_spent": 0.0, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [], - "priority": "3", - "code": "T11094", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 89735, - 89736, - 89740, - 89747 - ], - "message_ids": [ - 228108, - 228107, - 228106, - 228091, - 228086, - 228083 - ], - "message_main_attachment_id": [ - 144918, - "image.png" - ], - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "bdbe88e0-f951-416f-bec8-9bfda0441405", - "create_uid": [ - 28, - "Support Robot" - ], - "write_uid": [ - 32, - "Romuald GRUSON" - ], - "x_CBAO_windows_maj_ID": "", - "x_CBAO_version_signalement": "20250318", - "x_CBAO_version_correction": "", - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "romuald@mail.cbao.fr", - "attachment_ids": [], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 1, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/11073", - "access_warning": "", - "display_name": "[T11094] Calcul de la masse volumique", - "__last_update": "2025-03-21 14:50:41" -} \ No newline at end of file diff --git a/output_processed/ticket_T0167/messages.json b/output_processed/ticket_T0167/messages.json deleted file mode 100644 index 46ca034..0000000 --- a/output_processed/ticket_T0167/messages.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "id": "ticket_info", - "name": "Pb d'affaire/chantier/partie dans un programme d'essai", - "code": "T0167", - "description": "Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas.", - "date_create": "2020-04-27 06:21:36", - "role": "system", - "type": "contexte", - "body": "TICKET T0167: Pb d'affaire/chantier/partie dans un programme d'essai.\n\nDESCRIPTION: Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas." - }, - { - "id": "ticket_info", - "author_id": [ - 0, - "" - ], - "role": "Client", - "type": "Question", - "date": "", - "email_from": "", - "subject": "", - "body": "TICKET T0167: Pb d'affaire/chantier/partie dans un programme d'essai. DESCRIPTION: Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas." - }, - { - "id": "11333", - "author_id": [ - 10288, - "CBAO S.A.R.L., Youness BENDEQ" - ], - "role": "Support", - "type": "Réponse", - "date": "2020-04-27 06:20:22", - "email_from": "Youness BENDEQ ", - "subject": "Pb d'affaire/chantier/partie dans un programme d'essai", - "body": "-------- Message transféré -------- Sujet : De retour ! Date : Mon, 20 Apr 2020 14:52:05 +0000 De : LENEVEU Guillaume Pour : Youness BENDEQ Bonjour Youness, J’espère que tu vas bien ainsi que toute l’équipe BRG-LAB. Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas. Merci de ta réponse. Bonne fin de journée." - } -] \ No newline at end of file diff --git a/output_processed/ticket_T0167/pretraitement_rapport.json b/output_processed/ticket_T0167/pretraitement_rapport.json deleted file mode 100644 index 35a848a..0000000 --- a/output_processed/ticket_T0167/pretraitement_rapport.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "ticket_id": "ticket_T0167", - "fichiers_generes": [ - "ticket_info.json", - "messages.json" - ], - "erreurs": [] -} \ No newline at end of file diff --git a/output_processed/ticket_T0167/ticket_info.json b/output_processed/ticket_T0167/ticket_info.json deleted file mode 100644 index 55bed98..0000000 --- a/output_processed/ticket_T0167/ticket_info.json +++ /dev/null @@ -1,145 +0,0 @@ -{ - "id": 179, - "active": true, - "name": "Pb d'affaire/chantier/partie dans un programme d'essai", - "description": "Je viens vers toi car Mr NOVO m’a fait remonter un léger beug sur le numéro d’échantillon B2020-0001 (Voir PJ). En effet, il n’arrive pas à mettre le nom de la partie dans la partie ( en rouge sur la PJ). Il faudrait mettre « joint de chaussée côté giberville » comme stipulé dans le numéro d’affaire -> 20017 SETR -> LIAISON RD403 – RD402 DESSERTE PORTUAIRE VIADUC -> JOINT DE CHAUSSEE COTE GIBERVILLE. J’ai essayé de modifié la partie mais je n’y arrive pas.", - "sequence": 22, - "stage_id": [ - 8, - "Clôturé" - ], - "kanban_state": "normal", - "create_date": "2020-04-27 06:21:36", - "write_date": "2024-10-03 13:10:50", - "date_start": "2020-04-20 14:52:00", - "date_end": false, - "date_assign": "2020-04-27 07:15:48", - "date_deadline": false, - "date_last_stage_update": "2020-04-27 07:24:40", - "project_id": [ - 3, - "Demandes" - ], - "notes": false, - "planned_hours": 0.0, - "user_id": [ - 9, - "Youness BENDEQ" - ], - "partner_id": [ - 8504, - "CONSEIL DEPARTEMENTAL DU CALVADOS (14), Guillaume LENEVEU" - ], - "company_id": [ - 1, - "CBAO S.A.R.L." - ], - "color": 0, - "displayed_image_id": false, - "parent_id": false, - "child_ids": [], - "email_from": "guillaume.leneveu@calvados.fr", - "email_cc": "", - "working_hours_open": 0.0, - "working_hours_close": 0.0, - "working_days_open": 0.0, - "working_days_close": 0.0, - "website_message_ids": [ - 11333 - ], - "remaining_hours": -0.5, - "effective_hours": 0.5, - "total_hours_spent": 0.5, - "progress": 0.0, - "subtask_effective_hours": 0.0, - "timesheet_ids": [ - 51 - ], - "priority": "0", - "code": "T0167", - "milestone_id": false, - "sale_line_id": false, - "sale_order_id": false, - "billable_type": "no", - "activity_ids": [], - "message_follower_ids": [ - 10972 - ], - "message_ids": [ - 11346, - 11345, - 11344, - 11343, - 11342, - 11335, - 11334, - 11333, - 11332 - ], - "message_main_attachment_id": [ - 32380, - "image001.png" - ], - "failed_message_ids": [], - "rating_ids": [], - "rating_last_value": 0.0, - "access_token": "cd4fbf5c-27d3-48ed-8c9b-c07f20c3e2d4", - "create_uid": [ - 1, - "OdooBot" - ], - "write_uid": [ - 1, - "OdooBot" - ], - "x_CBAO_windows_maj_ID": false, - "x_CBAO_version_signalement": false, - "x_CBAO_version_correction": false, - "x_CBAO_DateCorrection": false, - "x_CBAO_Scoring_Facilite": 0, - "x_CBAO_Scoring_Importance": 0, - "x_CBAO_Scoring_Urgence": 0, - "x_CBAO_Scoring_Incidence": 0, - "x_CBAO_Scoring_Resultat": 0, - "x_CBAO_InformationsSup": false, - "kanban_state_label": "En cours", - "subtask_planned_hours": 0.0, - "manager_id": [ - 22, - "Fabien LAFAY" - ], - "user_email": "youness@cbao.fr", - "attachment_ids": [], - "legend_blocked": "Bloqué", - "legend_done": "Prêt pour la prochaine étape", - "legend_normal": "En cours", - "subtask_project_id": [ - 3, - "Demandes" - ], - "subtask_count": 0, - "analytic_account_active": true, - "allow_timesheets": true, - "use_milestones": false, - "show_time_control": "start", - "is_project_map_empty": true, - "activity_state": false, - "activity_user_id": false, - "activity_type_id": false, - "activity_date_deadline": false, - "activity_summary": false, - "message_is_follower": false, - "message_unread": false, - "message_unread_counter": 0, - "message_needaction": false, - "message_needaction_counter": 0, - "message_has_error": false, - "message_has_error_counter": 0, - "message_attachment_count": 2, - "rating_last_feedback": false, - "rating_count": 0, - "access_url": "/my/task/179", - "access_warning": "", - "display_name": "[T0167] Pb d'affaire/chantier/partie dans un programme d'essai", - "__last_update": "2024-10-03 13:10:50" -} \ No newline at end of file diff --git a/post_process.py b/post_process.py deleted file mode 100644 index bd12767..0000000 --- a/post_process.py +++ /dev/null @@ -1,982 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script de post-traitement pour améliorer les fichiers JSON avant analyse. -""" - -import os -import sys -import json -import re -import unicodedata -import shutil -from typing import Dict, List, Any, Optional - -def nettoyer_html(texte: str, debug: bool = False) -> str: - """ - Nettoie le contenu HTML en supprimant les balises et le formatage. - - Args: - texte: Texte HTML à nettoyer - debug: Afficher des informations de débogage pendant le nettoyage - - Returns: - Texte nettoyé - """ - # Vérifier et convertir l'entrée - if not texte: - return "" - - if not isinstance(texte, str): - try: - texte = str(texte) - except Exception as e: - print(f"AVERTISSEMENT: Impossible de convertir en texte: {e}") - return "" - - if debug: - print(f"Texte original ({len(texte)} caractères): {texte[:100]}...") - - # Détection de HTML - contient_html = bool(re.search(r'<[a-z]+[^>]*>', texte, re.IGNORECASE)) - if debug and contient_html: - print(f"Le texte contient du HTML, nettoyage nécessaire") - - # Supprimer les balises HTML - regex plus agressive pour capturer tous types de balises - try: - # Première passe - balises standard - texte_nettoye = re.sub(r']*>', ' ', texte, flags=re.IGNORECASE) - - # Deuxième passe - balises restantes, y compris les mal formées - texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye) - - if debug and contient_html: - print(f"Après suppression des balises HTML: {texte_nettoye[:100]}...") - except Exception as e: - print(f"AVERTISSEMENT: Erreur lors du nettoyage HTML: {e}") - texte_nettoye = texte - - # Remplacer les références aux images - try: - texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye) - texte_nettoye = re.sub(r']+>', '[Image]', texte_nettoye, flags=re.IGNORECASE) - except Exception as e: - if debug: - print(f"AVERTISSEMENT: Erreur lors du traitement des images: {e}") - - # Supprimer les éléments courants non pertinents - patterns_a_supprimer = [ - r'Cordialement,[\s\S]*?$', - r'Bien cordialement,[\s\S]*?$', - r'Bonne réception[\s\S]*?$', - r'À votre disposition[\s\S]*?$', - r'Support technique[\s\S]*?$', - r'L\'objectif du Support Technique[\s\S]*?$', - r'Notre service est ouvert[\s\S]*?$', - r'Dès réception[\s\S]*?$', - r'Confidentialité[\s\S]*?$', - r'Ce message électronique[\s\S]*?$', - r'Droit à la déconnexion[\s\S]*?$', - r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$', - r'tél\s*:\s*[\d\s\+]+', - r'mobile\s*:\s*[\d\s\+]+', - r'www\.[^\s]+\.[a-z]{2,3}', - ] - - try: - for pattern in patterns_a_supprimer: - texte_avant = texte_nettoye - texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE) - if debug and texte_avant != texte_nettoye: - print(f"Suppression de pattern '{pattern[:20]}...'") - except Exception as e: - # En cas d'échec des expressions régulières, conserver le texte tel quel - if debug: - print(f"AVERTISSEMENT: Erreur lors de la suppression des patterns: {e}") - - try: - # Supprimer les lignes multiples vides - texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye) - - # Supprimer les espaces multiples - texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye) - except Exception as e: - if debug: - print(f"AVERTISSEMENT: Erreur lors du nettoyage des espaces: {e}") - - # Normaliser les caractères accentués - try: - texte_nettoye = normaliser_accents(texte_nettoye) - except Exception as e: - print(f"AVERTISSEMENT: Erreur lors de la normalisation des accents: {e}") - - if debug: - print(f"Texte final ({len(texte_nettoye)} caractères): {texte_nettoye[:100]}...") - - return texte_nettoye.strip() - -def normaliser_accents(texte: str) -> str: - """ - Normalise les caractères accentués pour éviter les problèmes d'encodage. - - Args: - texte: Texte à normaliser - - Returns: - Texte avec caractères accentués normalisés - """ - # Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide - if not isinstance(texte, str): - if texte is None: - return "" - try: - texte = str(texte) - except: - return "" - - # Convertir les caractères spéciaux HTML (comme é) - special_chars = { - 'á': 'á', 'é': 'é', 'í': 'í', 'ó': 'ó', 'ú': 'ú', - 'Á': 'Á', 'É': 'É', 'Í': 'Í', 'Ó': 'Ó', 'Ú': 'Ú', - 'à': 'à', 'è': 'è', 'ì': 'ì', 'ò': 'ò', 'ù': 'ù', - 'À': 'À', 'È': 'È', 'Ì': 'Ì', 'Ò': 'Ò', 'Ù': 'Ù', - 'â': 'â', 'ê': 'ê', 'î': 'î', 'ô': 'ô', 'û': 'û', - 'Â': 'Â', 'Ê': 'Ê', 'Î': 'Î', 'Ô': 'Ô', 'Û': 'Û', - 'ã': 'ã', '&etilde;': 'ẽ', 'ĩ': 'ĩ', 'õ': 'õ', 'ũ': 'ũ', - 'Ã': 'Ã', '&Etilde;': 'Ẽ', 'Ĩ': 'Ĩ', 'Õ': 'Õ', 'Ũ': 'Ũ', - 'ä': 'ä', 'ë': 'ë', 'ï': 'ï', 'ö': 'ö', 'ü': 'ü', - 'Ä': 'Ä', 'Ë': 'Ë', 'Ï': 'Ï', 'Ö': 'Ö', 'Ü': 'Ü', - 'ç': 'ç', 'Ç': 'Ç', 'ñ': 'ñ', 'Ñ': 'Ñ', - ' ': ' ', '<': '<', '>': '>', '&': '&', '"': '"', ''': "'", - '€': '€', '©': '©', '®': '®', '™': '™' - } - - for html, char in special_chars.items(): - texte = texte.replace(html, char) - - # Normaliser les caractères composés (par exemple, e plus accent) - # Exemple: 'é' qui pourrait être stocké comme 'e' + accent combinant - return unicodedata.normalize('NFC', texte) - -def detecter_role(message: Dict[str, Any]) -> str: - """ - Détecte si un message provient du client ou du support. - - Args: - message: Dictionnaire contenant les informations du message - - Returns: - "Client" ou "Support" - """ - # Vérifier le champ 'role' s'il existe déjà - if "role" in message and message["role"] in ["Client", "Support"]: - return message["role"] - - # Indices de support dans l'email - domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"] - indices_nom_support = ["support", "cbao", "technique", "odoo"] - - email = message.get("email_from", "").lower() - # Nettoyer le format "Nom " - if "<" in email and ">" in email: - match = re.search(r'<([^>]+)>', email) - if match: - email = match.group(1).lower() - - # Vérifier le domaine email - if any(domaine in email for domaine in domaines_support): - return "Support" - - # Vérifier le nom d'auteur - auteur = "" - if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1: - auteur = str(message["author_id"][1]).lower() - - if any(indice in auteur for indice in indices_nom_support): - return "Support" - - # Par défaut, considérer comme client - return "Client" - -def transformer_messages(input_file: str, output_file: Optional[str] = None, debug: bool = False) -> None: - """ - Transforme le fichier messages.json en un format amélioré pour l'analyse LLM. - - Args: - input_file: Chemin du fichier messages.json original - output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée) - debug: Activer le mode débogage pour afficher plus d'informations - """ - if output_file is None: - output_file = input_file - - if debug: - print(f"Transformation du fichier {input_file} vers {output_file} (mode débogage activé)") - - try: - # Lire le fichier messages.json original - with open(input_file, 'r', encoding='utf-8') as f: - messages = json.load(f) - - # Trouver le répertoire du ticket et charger les informations - ticket_dir = os.path.dirname(input_file) - ticket_info_path = os.path.join(ticket_dir, "ticket_info.json") - ticket_info = {} - - # Extraire le code du ticket du nom du répertoire - try: - dir_name = os.path.basename(ticket_dir) - ticket_code = dir_name.replace("ticket_", "") if dir_name.startswith("ticket_") else dir_name - except Exception as e: - print(f"AVERTISSEMENT: Impossible d'extraire le code du ticket du répertoire: {e}") - ticket_code = "UNKNOWN" - - # Charger les informations du ticket - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - except Exception as e: - print(f"AVERTISSEMENT: Impossible de charger ticket_info.json: {e}") - - # Créer une version améliorée des messages - processed_messages = [] - - # Extraire et formater les informations du ticket de manière sécurisée - ticket_name = "" - ticket_description = "" - ticket_date = "" - - try: - if ticket_info: - ticket_name = str(ticket_info.get("name", "")) if ticket_info.get("name") is not None else "" - ticket_description = str(ticket_info.get("description", "")) if ticket_info.get("description") is not None else "" - ticket_date = str(ticket_info.get("create_date", "")) if ticket_info.get("create_date") is not None else "" - ticket_code = str(ticket_info.get("code", ticket_code)) if ticket_info.get("code") is not None else ticket_code - except Exception as e: - print(f"AVERTISSEMENT: Problème lors de l'extraction des données du ticket: {e}") - - # Nettoyer la description pour éliminer les balises HTML - ticket_description_nettoyee = nettoyer_html(ticket_description, debug=debug) - if debug: - print(f"Description originale: {ticket_description[:100]}...") - print(f"Description nettoyée: {ticket_description_nettoyee[:100]}...") - - # Ajouter les informations du ticket comme premier "message" - formatted_ticket_info = { - "id": "ticket_info", - "name": normaliser_accents(ticket_name) or f"Ticket {ticket_code}", - "code": ticket_code, - "description": ticket_description_nettoyee, - "date_create": ticket_date, - "role": "system", - "type": "contexte", - "body": f"TICKET {ticket_code}: {normaliser_accents(ticket_name)}.\n\nDESCRIPTION: {ticket_description_nettoyee or 'Aucune description disponible.'}" - } - processed_messages.append(formatted_ticket_info) - - # Vérifier que messages est bien une liste - if not isinstance(messages, list): - print(f"AVERTISSEMENT: Le fichier messages.json ne contient pas une liste valide. Type: {type(messages)}") - messages = [] - - # Transformer chaque message - valid_messages = 0 - for msg in messages: - # Vérifier que msg est un dictionnaire - if not isinstance(msg, dict): - print(f"AVERTISSEMENT: Message ignoré car ce n'est pas un dictionnaire. Type: {type(msg)}") - continue - - # Ignorer les messages vides - body = msg.get("body", "") - if not body or not isinstance(body, str): - continue - - if debug: - contient_html = bool(re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE)) - if contient_html: - print(f"Message {msg.get('id', 'unknown')} contient du HTML") - - # Déterminer le type (question/réponse) basé sur le rôle - role = detecter_role(msg) - message_type = "Question" if role == "Client" else "Réponse" - - # Nettoyer le contenu de manière sécurisée - contenu_nettoye = nettoyer_html(body, debug=debug) - if not contenu_nettoye: - if debug: - print(f"Message {msg.get('id', 'unknown')} ignoré - contenu vide après nettoyage") - continue - - # Normaliser les champs textuels - email_from = normaliser_accents(msg.get("email_from", "")) - subject = normaliser_accents(msg.get("subject", "")) - - # Créer le message transformé avec des valeurs sécurisées - msg_id = msg.get("id", "") - if not msg_id: - msg_id = f"msg_{valid_messages+1}" - - # Convertir msg_id en string si ce n'est pas le cas - if not isinstance(msg_id, str): - try: - msg_id = str(msg_id) - except: - msg_id = f"msg_{valid_messages+1}" - - # Récupérer author_id de manière sécurisée - author_id = msg.get("author_id", [0, ""]) - if not isinstance(author_id, list): - author_id = [0, ""] - - # Récupérer la date de manière sécurisée - date = msg.get("date", "") - if not isinstance(date, str): - try: - date = str(date) - except: - date = "" - - processed_message = { - "id": msg_id, - "author_id": author_id, - "role": role, - "type": message_type, - "date": date, - "email_from": email_from, - "subject": subject, - "body": contenu_nettoye - } - - processed_messages.append(processed_message) - valid_messages += 1 - - # Trier par date (sauf le premier message qui est le contexte) - try: - processed_messages[1:] = sorted(processed_messages[1:], key=lambda x: x.get("date", "")) - except Exception as e: - print(f"AVERTISSEMENT: Impossible de trier les messages par date: {e}") - - # Vérifier qu'il y a au moins un message valide en plus du contexte - if valid_messages == 0: - print("AVERTISSEMENT: Aucun message valide trouvé après nettoyage.") - # Ajouter un message factice pour éviter les erreurs - processed_messages.append({ - "id": "msg_default", - "role": "Client", - "type": "Question", - "date": formatted_ticket_info.get("date_create", ""), - "body": f"Problème concernant {formatted_ticket_info.get('name', 'ce ticket')}." - }) - - # Écrire le fichier transformé - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(processed_messages, f, indent=2, ensure_ascii=False) - - print(f"Transformation réussie: {len(processed_messages)} messages traités ({valid_messages} messages réels)") - - except Exception as e: - print(f"Erreur lors de la transformation des messages: {str(e)}") - import traceback - print(f"Détails: {traceback.format_exc()}") - raise - -def corriger_json_accents(input_file: str, output_file: Optional[str] = None) -> None: - """ - Corrige les problèmes d'accents dans un fichier JSON. - - Args: - input_file: Chemin du fichier JSON à corriger - output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée) - """ - if output_file is None: - output_file = input_file - - try: - # Lire le fichier JSON - with open(input_file, 'r', encoding='utf-8') as f: - content = json.load(f) - - # Fonction récursive pour normaliser tous les textes dans le JSON - def normaliser_json(obj): - if isinstance(obj, str): - return normaliser_accents(obj) - elif isinstance(obj, list): - return [normaliser_json(item) for item in obj] - elif isinstance(obj, dict): - return {k: normaliser_json(v) for k, v in obj.items()} - else: - return obj - - # Normaliser tout le contenu JSON - content_normalise = normaliser_json(content) - - # Écrire le fichier normalisé - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(content_normalise, f, indent=2, ensure_ascii=False) - - print(f"Correction des accents réussie pour {os.path.basename(input_file)}") - - except Exception as e: - print(f"Erreur lors de la correction des accents: {str(e)}") - raise - -def corriger_markdown_accents(input_file: str, output_file: Optional[str] = None) -> None: - """ - Corrige les problèmes d'accents dans un fichier Markdown. - - Args: - input_file: Chemin du fichier Markdown à corriger - output_file: Chemin du fichier de sortie (par défaut, écrase le fichier d'entrée) - """ - if output_file is None: - output_file = input_file - - try: - # Lire le fichier Markdown - with open(input_file, 'r', encoding='utf-8') as f: - content = f.read() - - # Normaliser les accents - content_normalise = normaliser_accents(content) - - # Vérifier si des changements ont été effectués - if content != content_normalise: - # Écrire le fichier normalisé - with open(output_file, 'w', encoding='utf-8') as f: - f.write(content_normalise) - - print(f"Correction des accents réussie pour {os.path.basename(input_file)}") - else: - print(f"Aucune correction nécessaire pour {os.path.basename(input_file)}") - - except Exception as e: - print(f"Erreur lors de la correction des accents dans le markdown: {str(e)}") - raise - -def reparer_ticket(ticket_dir: str) -> bool: - """ - Répare et réinitialise le traitement d'un ticket dont les données sont corrompues. - - Args: - ticket_dir: Chemin du répertoire du ticket - - Returns: - True si la réparation a réussi, False sinon - """ - try: - print(f"Tentative de réparation du ticket dans {ticket_dir}...") - - # Vérifier que le répertoire existe - if not os.path.isdir(ticket_dir): - print(f"ERREUR: Répertoire de ticket introuvable: {ticket_dir}") - return False - - # Chemins des fichiers critiques - ticket_info_path = os.path.join(ticket_dir, "ticket_info.json") - messages_path = os.path.join(ticket_dir, "messages.json") - attachments_path = os.path.join(ticket_dir, "attachments_info.json") - - # Vérifier et réparer ticket_info.json - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - # Vérifier la structure minimale - if not isinstance(ticket_info, dict): - raise ValueError("ticket_info.json n'est pas un dictionnaire valide") - - # Réparer les champs manquants ou invalides - code = os.path.basename(ticket_dir).replace("ticket_", "") - if "code" not in ticket_info or not isinstance(ticket_info["code"], str): - ticket_info["code"] = code - - if "name" not in ticket_info or not isinstance(ticket_info["name"], str): - ticket_info["name"] = f"Ticket {code}" - - if "description" not in ticket_info or not isinstance(ticket_info["description"], str): - ticket_info["description"] = "" - - # Réécrire le fichier nettoyé - with open(ticket_info_path, 'w', encoding='utf-8') as f: - json.dump(ticket_info, f, indent=2, ensure_ascii=False) - - print(f"✓ ticket_info.json réparé") - - except Exception as e: - print(f"! Erreur lors de la réparation de ticket_info.json: {str(e)}") - # Créer un ticket_info minimal - ticket_info = { - "code": os.path.basename(ticket_dir).replace("ticket_", ""), - "name": f"Ticket {os.path.basename(ticket_dir).replace('ticket_', '')}", - "description": "", - "create_date": "" - } - - # Sauvegarder la version minimale - with open(ticket_info_path, 'w', encoding='utf-8') as f: - json.dump(ticket_info, f, indent=2, ensure_ascii=False) - - print(f"✓ ticket_info.json recréé avec structure minimale") - else: - # Créer un ticket_info minimal - ticket_info = { - "code": os.path.basename(ticket_dir).replace("ticket_", ""), - "name": f"Ticket {os.path.basename(ticket_dir).replace('ticket_', '')}", - "description": "", - "create_date": "" - } - - # Sauvegarder la version minimale - with open(ticket_info_path, 'w', encoding='utf-8') as f: - json.dump(ticket_info, f, indent=2, ensure_ascii=False) - - print(f"✓ ticket_info.json créé avec structure minimale") - - # Vérifier et réparer messages.json - messages_valides = False - if os.path.exists(messages_path): - try: - # Sauvegarder l'original s'il n'y a pas encore de backup - backup_file = os.path.join(ticket_dir, "messages.json.original") - if not os.path.exists(backup_file): - shutil.copy2(messages_path, backup_file) - print(f"✓ Sauvegarde originale créée: {backup_file}") - - # Essayer de charger le fichier - with open(messages_path, 'r', encoding='utf-8') as f: - messages = json.load(f) - - # Vérifier que c'est une liste - if not isinstance(messages, list): - raise ValueError("messages.json n'est pas une liste valide") - - messages_valides = True - print(f"✓ messages.json valide ({len(messages)} messages)") - except Exception as e: - print(f"! Erreur dans messages.json: {str(e)}") - print(" Tentative de récupération...") - - # Essayer de récupérer depuis la sauvegarde - backup_file = os.path.join(ticket_dir, "messages.json.original") - if os.path.exists(backup_file): - try: - with open(backup_file, 'r', encoding='utf-8') as f: - messages = json.load(f) - - if isinstance(messages, list): - # Sauvegarder la version récupérée - with open(messages_path, 'w', encoding='utf-8') as f: - json.dump(messages, f, indent=2, ensure_ascii=False) - - messages_valides = True - print(f"✓ messages.json récupéré depuis la sauvegarde") - except Exception: - print(" Échec de la récupération depuis la sauvegarde") - - # Si les messages sont toujours invalides, créer un fichier minimal - if not messages_valides: - # Créer un fichier messages minimal - messages = [{ - "id": 1, - "body": f"Message par défaut pour le ticket {os.path.basename(ticket_dir)}", - "date": "", - "email_from": "client@example.com" - }] - - # Sauvegarder la version minimale - with open(messages_path, 'w', encoding='utf-8') as f: - json.dump(messages, f, indent=2, ensure_ascii=False) - - print(f"✓ messages.json recréé avec message par défaut") - - # Transformer messages.json pour le format attendu - print("Transformation des messages pour le bon format...") - transformer_messages(messages_path) - print("✓ Transformation des messages terminée") - - # Vérifier et réparer attachments_info.json - if os.path.exists(attachments_path): - try: - with open(attachments_path, 'r', encoding='utf-8') as f: - attachments = json.load(f) - - # Vérifier que c'est une liste - if not isinstance(attachments, list): - attachments = [] - with open(attachments_path, 'w', encoding='utf-8') as f: - json.dump(attachments, f, indent=2, ensure_ascii=False) - print(f"✓ attachments_info.json réparé (liste vide)") - else: - print(f"✓ attachments_info.json valide ({len(attachments)} pièces jointes)") - except Exception as e: - print(f"! Erreur dans attachments_info.json: {str(e)}") - # Créer une liste vide - with open(attachments_path, 'w', encoding='utf-8') as f: - json.dump([], f, indent=2, ensure_ascii=False) - print(f"✓ attachments_info.json recréé (liste vide)") - else: - # Créer une liste vide - with open(attachments_path, 'w', encoding='utf-8') as f: - json.dump([], f, indent=2, ensure_ascii=False) - print(f"✓ attachments_info.json créé (liste vide)") - - print(f"Réparation du ticket terminée avec succès!") - return True - - except Exception as e: - print(f"ERREUR lors de la réparation du ticket: {str(e)}") - import traceback - print(f"Détails: {traceback.format_exc()}") - return False - -def diagnostiquer_ticket(ticket_dir: str) -> Dict[str, Any]: - """ - Diagnostique les problèmes dans un ticket et propose des solutions. - - Args: - ticket_dir: Chemin du répertoire du ticket - - Returns: - Rapport de diagnostic avec les problèmes identifiés et solutions proposées - """ - diagnostic = { - "problemes": [], - "suggestions": [], - "etat_fichiers": {} - } - - print(f"Diagnostic du ticket dans {ticket_dir}...") - - # Vérifier que le répertoire existe - if not os.path.isdir(ticket_dir): - diagnostic["problemes"].append(f"Répertoire de ticket introuvable: {ticket_dir}") - diagnostic["suggestions"].append("Créer le répertoire du ticket") - return diagnostic - - # Chemins des fichiers critiques - ticket_info_path = os.path.join(ticket_dir, "ticket_info.json") - messages_path = os.path.join(ticket_dir, "messages.json") - messages_backup_path = os.path.join(ticket_dir, "messages.json.backup") - attachments_path = os.path.join(ticket_dir, "attachments_info.json") - attachments_dir = os.path.join(ticket_dir, "attachments") - questions_reponses_path = os.path.join(ticket_dir, "questions_reponses.md") - rapport_dir = os.path.join(ticket_dir, "rapport") - - # Vérifier ticket_info.json - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - diagnostic["etat_fichiers"]["ticket_info.json"] = "valide" - - # Vérifier la structure minimale - if not isinstance(ticket_info, dict): - diagnostic["problemes"].append("ticket_info.json n'est pas un dictionnaire valide") - diagnostic["suggestions"].append("Réparer ticket_info.json avec --repair") - diagnostic["etat_fichiers"]["ticket_info.json"] = "invalide" - - # Vérifier les champs HTML - description = ticket_info.get("description", "") - if isinstance(description, str) and re.search(r'<[a-z]+[^>]*>', description, re.IGNORECASE): - diagnostic["problemes"].append("La description contient du HTML non traité") - diagnostic["suggestions"].append("Traiter les balises HTML dans la description") - - except Exception as e: - diagnostic["problemes"].append(f"Erreur dans ticket_info.json: {str(e)}") - diagnostic["suggestions"].append("Réparer ticket_info.json avec --repair") - diagnostic["etat_fichiers"]["ticket_info.json"] = "corrompu" - else: - diagnostic["problemes"].append(f"Fichier manquant: ticket_info.json") - diagnostic["suggestions"].append("Créer ticket_info.json avec --repair") - diagnostic["etat_fichiers"]["ticket_info.json"] = "manquant" - - # Vérifier messages.json - if os.path.exists(messages_path): - try: - with open(messages_path, 'r', encoding='utf-8') as f: - messages = json.load(f) - - diagnostic["etat_fichiers"]["messages.json"] = "valide" - - # Vérifier que c'est une liste - if not isinstance(messages, list): - diagnostic["problemes"].append("messages.json n'est pas une liste valide") - diagnostic["suggestions"].append("Réparer messages.json avec --repair") - diagnostic["etat_fichiers"]["messages.json"] = "invalide" - - # Vérifier le contenu HTML dans les messages - html_count = 0 - for msg in messages: - if not isinstance(msg, dict): - continue - - body = msg.get("body", "") - if isinstance(body, str) and re.search(r'<[a-z]+[^>]*>', body, re.IGNORECASE): - html_count += 1 - - if html_count > 0: - diagnostic["problemes"].append(f"{html_count} message(s) contiennent du HTML non traité") - diagnostic["suggestions"].append("Retraiter messages.json avec --debug pour voir les problèmes") - - # Vérifier les accents dans les messages - accents_count = 0 - for msg in messages: - if not isinstance(msg, dict): - continue - - body = msg.get("body", "") - if isinstance(body, str): - # Vérifier les entités HTML pour les accents - if re.search(r'&[aeiounc][a-z]{3,5};', body, re.IGNORECASE): - accents_count += 1 - - if accents_count > 0: - diagnostic["problemes"].append(f"{accents_count} message(s) contiennent des entités HTML d'accent non converties") - diagnostic["suggestions"].append("Corriger les accents avec --fix-all") - - except Exception as e: - diagnostic["problemes"].append(f"Erreur dans messages.json: {str(e)}") - diagnostic["suggestions"].append("Réparer messages.json avec --repair") - diagnostic["etat_fichiers"]["messages.json"] = "corrompu" - else: - diagnostic["problemes"].append(f"Fichier manquant: messages.json") - diagnostic["suggestions"].append("Créer messages.json avec --repair") - diagnostic["etat_fichiers"]["messages.json"] = "manquant" - - # Vérifier si une sauvegarde des messages existe - if os.path.exists(messages_backup_path): - diagnostic["etat_fichiers"]["messages.json.backup"] = "présent" - else: - diagnostic["etat_fichiers"]["messages.json.backup"] = "manquant" - - # Vérifier le fichier des questions et réponses - if os.path.exists(questions_reponses_path): - try: - with open(questions_reponses_path, 'r', encoding='utf-8') as f: - content = f.read() - - diagnostic["etat_fichiers"]["questions_reponses.md"] = "présent" - - # Vérifier si des questions/réponses sont présentes - if "| Question | Réponse |" in content and not re.search(r'\| \*\*[^|]+\*\*: ', content): - diagnostic["problemes"].append("Le fichier questions_reponses.md ne contient pas de questions/réponses") - diagnostic["suggestions"].append("Retraiter le ticket pour extraire les questions/réponses") - - except Exception as e: - diagnostic["problemes"].append(f"Erreur dans questions_reponses.md: {str(e)}") - diagnostic["etat_fichiers"]["questions_reponses.md"] = "invalide" - else: - diagnostic["etat_fichiers"]["questions_reponses.md"] = "manquant" - - # Vérifier les pièces jointes - if os.path.exists(attachments_path): - try: - with open(attachments_path, 'r', encoding='utf-8') as f: - attachments = json.load(f) - - diagnostic["etat_fichiers"]["attachments_info.json"] = "valide" - - # Vérifier que c'est une liste - if not isinstance(attachments, list): - diagnostic["problemes"].append("attachments_info.json n'est pas une liste valide") - diagnostic["suggestions"].append("Réparer attachments_info.json avec --repair") - diagnostic["etat_fichiers"]["attachments_info.json"] = "invalide" - - # Vérifier que les fichiers attachés existent - if os.path.exists(attachments_dir): - diagnostic["etat_fichiers"]["attachments/"] = "présent" - - for attachment in attachments: - if not isinstance(attachment, dict): - continue - - file_path = attachment.get("file_path", "") - if not file_path: - continue - - # Normaliser le chemin - if not os.path.isabs(file_path): - file_path = os.path.join(attachments_dir, os.path.basename(file_path)) - - if not os.path.exists(file_path): - file_name = os.path.basename(file_path) - diagnostic["problemes"].append(f"Fichier attaché manquant: {file_name}") - else: - diagnostic["etat_fichiers"]["attachments/"] = "manquant" - diagnostic["problemes"].append("Répertoire attachments/ manquant") - - except Exception as e: - diagnostic["problemes"].append(f"Erreur dans attachments_info.json: {str(e)}") - diagnostic["suggestions"].append("Réparer attachments_info.json avec --repair") - diagnostic["etat_fichiers"]["attachments_info.json"] = "corrompu" - else: - diagnostic["etat_fichiers"]["attachments_info.json"] = "manquant" - diagnostic["problemes"].append("Fichier attachments_info.json manquant") - diagnostic["suggestions"].append("Créer attachments_info.json avec --repair") - - # Vérifier le répertoire rapport - if os.path.exists(rapport_dir): - diagnostic["etat_fichiers"]["rapport/"] = "présent" - - rapport_json = os.path.join(rapport_dir, "ticket_analysis.json") - rapport_md = os.path.join(rapport_dir, "ticket_analysis.md") - - if os.path.exists(rapport_json): - diagnostic["etat_fichiers"]["rapport/ticket_analysis.json"] = "présent" - else: - diagnostic["etat_fichiers"]["rapport/ticket_analysis.json"] = "manquant" - diagnostic["problemes"].append("Rapport JSON manquant") - - if os.path.exists(rapport_md): - diagnostic["etat_fichiers"]["rapport/ticket_analysis.md"] = "présent" - else: - diagnostic["etat_fichiers"]["rapport/ticket_analysis.md"] = "manquant" - diagnostic["problemes"].append("Rapport Markdown manquant") - else: - diagnostic["etat_fichiers"]["rapport/"] = "manquant" - - # Ajouter des suggestions globales si nécessaires - if len(diagnostic["problemes"]) > 3: - diagnostic["suggestions"].insert(0, "Utiliser l'option --repair pour essayer de corriger tous les problèmes automatiquement") - - # Afficher le résumé du diagnostic - print(f"\nRésumé du diagnostic pour {os.path.basename(ticket_dir)}:") - print(f"- Problèmes identifiés: {len(diagnostic['problemes'])}") - - for i, probleme in enumerate(diagnostic["problemes"]): - print(f" {i+1}. {probleme}") - - print("\nSuggestions:") - for suggestion in diagnostic["suggestions"]: - print(f"- {suggestion}") - - return diagnostic - -def main(): - """ - Point d'entrée principal du script. - """ - # Analyser les arguments - if len(sys.argv) < 2: - print("Usage: python post_process.py [options]") - print("Options:") - print(" --fix-all Corriger les accents dans tous les fichiers JSON et Markdown") - print(" --fix-md Corriger uniquement les fichiers Markdown") - print(" --repair Réparer un ticket corrompu") - print(" --debug Activer le mode débogage") - print(" --diagnose Diagnostiquer les problèmes du ticket") - print(" --help Afficher cette aide") - sys.exit(1) - - # Afficher l'aide - if "--help" in sys.argv: - print("Usage: python post_process.py [options]") - print("Options:") - print(" --fix-all Corriger les accents dans tous les fichiers JSON et Markdown") - print(" --fix-md Corriger uniquement les fichiers Markdown") - print(" --repair Réparer un ticket corrompu") - print(" --debug Activer le mode débogage") - print(" --diagnose Diagnostiquer les problèmes du ticket") - print(" --help Afficher cette aide") - sys.exit(0) - - ticket_dir = sys.argv[1] - fix_all = "--fix-all" in sys.argv - fix_md = "--fix-md" in sys.argv - repair = "--repair" in sys.argv - debug = "--debug" in sys.argv - diagnose = "--diagnose" in sys.argv - - # Vérifier que le répertoire existe - if not os.path.isdir(ticket_dir): - print(f"ERREUR: Répertoire non trouvé: {ticket_dir}") - sys.exit(1) - - # Option de diagnostic du ticket - if diagnose: - diagnostiquer_ticket(ticket_dir) - sys.exit(0) - - # Option de réparation du ticket - if repair: - success = reparer_ticket(ticket_dir) - if not success: - print("La réparation du ticket a échoué.") - sys.exit(1) - print("Ticket réparé avec succès!") - sys.exit(0) - - # Option de correction des accents dans les fichiers Markdown uniquement - if fix_md: - rapport_dir = os.path.join(ticket_dir, "rapport") - corrected = False - - # Corriger les fichiers Markdown du répertoire rapport - if os.path.exists(rapport_dir): - for root, _, files in os.walk(rapport_dir): - for file in files: - if file.endswith(".md"): - md_file = os.path.join(root, file) - corriger_markdown_accents(md_file) - corrected = True - - # Corriger les fichiers Markdown à la racine du ticket - for file in os.listdir(ticket_dir): - if file.endswith(".md"): - md_file = os.path.join(ticket_dir, file) - corriger_markdown_accents(md_file) - corrected = True - - if corrected: - print("Correction des accents terminée dans les fichiers Markdown.") - else: - print("Aucun fichier Markdown trouvé.") - sys.exit(0) - - # Transformation standard des messages - messages_file = os.path.join(ticket_dir, "messages.json") - if not os.path.exists(messages_file): - print(f"Fichier non trouvé: {messages_file}") - sys.exit(1) - - try: - # Transformer les messages - transformer_messages(messages_file, debug=debug) - print(f"Post-traitement terminé pour {messages_file}") - - # Corriger les accents dans tous les fichiers si demandé - if fix_all: - rapport_dir = os.path.join(ticket_dir, "rapport") - if os.path.exists(rapport_dir): - # Corriger les fichiers JSON - for root, _, files in os.walk(rapport_dir): - for file in files: - if file.endswith(".json"): - json_file = os.path.join(root, file) - corriger_json_accents(json_file) - - # Corriger les fichiers Markdown - for root, _, files in os.walk(rapport_dir): - for file in files: - if file.endswith(".md"): - md_file = os.path.join(root, file) - corriger_markdown_accents(md_file) - - # Corriger les fichiers Markdown à la racine du ticket - for file in os.listdir(ticket_dir): - if file.endswith(".md"): - md_file = os.path.join(ticket_dir, file) - corriger_markdown_accents(md_file) - - print("Correction des accents terminée dans tous les fichiers.") - except Exception as e: - print(f"ERREUR lors du post-traitement: {str(e)}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5dd5943..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -requests>=2.28.0 -mistralai>=0.0.7 \ No newline at end of file diff --git a/scripts/analyze_image_contexte.py b/scripts/analyze_image_contexte.py deleted file mode 100644 index 2378636..0000000 --- a/scripts/analyze_image_contexte.py +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script d'analyse d'image avec contexte pour les tickets de support. -Extrait des informations pertinentes d'une image en fonction du contexte du ticket. -""" - -import os -import sys -import json -import argparse -import logging -from typing import Dict, Any, Optional - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("analyze_image.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("analyze_image") - -try: - from llm import Pixtral -except ImportError: - logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.") - sys.exit(1) - -class ImageAnalyzer: - """ - Analyseur d'image qui extrait des informations pertinentes en fonction du contexte. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'analyseur d'image. - - Args: - api_key: Clé API pour le modèle de vision - """ - self.llm = Pixtral(api_key=api_key) - - # Configurer le modèle de vision - try: - self.llm.model = "pixtral-12b-2409" - self.llm.temperature = 0.3 - self.llm.max_tokens = 1024 - except Exception as e: - logger.warning(f"Impossible de configurer le modèle: {e}") - - self.historique = [] - - def ajouter_historique(self, action: str, entree: str, resultat: str) -> None: - """ - Ajoute une entrée à l'historique des actions. - - Args: - action: Type d'action effectuée - entree: Entrée de l'action - resultat: Résultat de l'action - """ - self.historique.append({ - "action": action, - "entree": entree, - "resultat": resultat - }) - - def analyser_image(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]: - """ - Analyse une image en fonction du contexte donné. - - Args: - image_path: Chemin vers l'image à analyser - contexte: Contexte du ticket pour aider à l'analyse - - Returns: - Résultat de l'analyse de l'image - """ - if not os.path.exists(image_path): - logger.error(f"Image introuvable: {image_path}") - return { - "success": False, - "erreur": "Image introuvable", - "path": image_path - } - - # Vérifier que le fichier est une image - _, extension = os.path.splitext(image_path) - if extension.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']: - logger.error(f"Format de fichier non supporté: {extension}") - return { - "success": False, - "erreur": f"Format de fichier non supporté: {extension}", - "path": image_path - } - - # Préparer le prompt pour l'analyse - prompt_base = """ - Tu es un expert en analyse technique d'interfaces utilisateur et de captures d'écran. - - Analyse cette image en détail et extrait les informations suivantes: - - 1. Type d'image: capture d'écran, photo, schéma, etc. - 2. Interface visible: nom du logiciel, type d'interface, fonctionnalités visibles - 3. Éléments importants: boutons, menus, messages d'erreur, données visibles - 4. Problème potentiel: erreurs, anomalies, incohérences visibles - 5. Contexte technique: environnement logiciel, version potentielle, plateforme - - Pour les captures d'écran, identifie précisément: - - Le nom exact de la fenêtre/dialogue - - Les champs/formulaires visibles - - Les valeurs/données affichées - - Les messages d'erreur ou d'avertissement - - Les boutons/actions disponibles - - Réponds de manière structurée en format Markdown avec des sections claires. - Sois précis et factuel, en te concentrant sur les éléments techniques visibles. - """ - - # Ajouter le contexte si disponible - if contexte: - prompt_base += f""" - - CONTEXTE DU TICKET: - {contexte} - - En tenant compte du contexte ci-dessus, explique également: - - En quoi cette image est pertinente pour le problème décrit - - Quels éléments de l'image correspondent au problème mentionné - - Comment cette image peut aider à résoudre le problème - """ - - try: - # Appeler le modèle de vision - try: - resultat = self.llm.analyze_image(image_path, prompt_base) - self.ajouter_historique("analyze_image", os.path.basename(image_path), "Analyse effectuée") - except Exception as e: - logger.error(f"Erreur lors de l'appel au modèle de vision: {str(e)}") - return { - "success": False, - "erreur": f"Erreur lors de l'appel au modèle de vision: {str(e)}", - "path": image_path - } - - # Extraire le contenu de la réponse - contenu = resultat.get("content", "") - if not contenu: - logger.error("Réponse vide du modèle de vision") - return { - "success": False, - "erreur": "Réponse vide du modèle de vision", - "path": image_path - } - - # Créer le résultat final - resultat_analyse = { - "success": True, - "path": image_path, - "analyse": contenu, - "contexte_fourni": bool(contexte) - } - - # Essayer d'extraire des informations structurées à partir de l'analyse - try: - # Rechercher le type d'image - import re - type_match = re.search(r"Type d['']image\s*:\s*([^\n\.]+)", contenu, re.IGNORECASE) - if type_match: - resultat_analyse["type_image"] = type_match.group(1).strip() - - # Rechercher l'interface - interface_match = re.search(r'Interface\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) - interface_match2 = re.search(r'Interface visible\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) - if interface_match: - resultat_analyse["interface"] = interface_match.group(1).strip() - elif interface_match2: - resultat_analyse["interface"] = interface_match2.group(1).strip() - - # Rechercher le problème - probleme_match = re.search(r'Problème\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) - probleme_match2 = re.search(r'Problème potentiel\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) - if probleme_match: - resultat_analyse["probleme"] = probleme_match.group(1).strip() - elif probleme_match2: - resultat_analyse["probleme"] = probleme_match2.group(1).strip() - except Exception as e: - logger.warning(f"Impossible d'extraire des informations structurées: {str(e)}") - - return resultat_analyse - - except Exception as e: - logger.error(f"Erreur lors de l'analyse de l'image {image_path}: {str(e)}") - return { - "success": False, - "erreur": str(e), - "path": image_path - } - - def generer_rapport_markdown(self, analyse: Dict[str, Any]) -> str: - """ - Génère un rapport Markdown à partir de l'analyse d'image. - - Args: - analyse: Résultat de l'analyse d'image - - Returns: - Rapport au format Markdown - """ - if not analyse.get("success", False): - return f"# Échec de l'analyse d'image\n\nErreur: {analyse.get('erreur', 'Inconnue')}\n\nImage: {analyse.get('path', 'Inconnue')}" - - # En-tête du rapport - image_path = analyse.get("path", "Inconnue") - image_name = os.path.basename(image_path) - - rapport = f"# Analyse de l'image: {image_name}\n\n" - - # Ajouter l'analyse brute - rapport += analyse.get("analyse", "Aucune analyse disponible") - - # Ajouter des métadonnées - rapport += "\n\n## Métadonnées\n\n" - rapport += f"- **Chemin de l'image**: `{image_path}`\n" - rapport += f"- **Contexte fourni**: {'Oui' if analyse.get('contexte_fourni', False) else 'Non'}\n" - - if "type_image" in analyse: - rapport += f"- **Type d'image détecté**: {analyse['type_image']}\n" - - if "interface" in analyse: - rapport += f"- **Interface identifiée**: {analyse['interface']}\n" - - if "probleme" in analyse: - rapport += f"- **Problème détecté**: {analyse['probleme']}\n" - - # Ajouter les paramètres du modèle - rapport += "\n## Paramètres du modèle\n\n" - rapport += f"- **Modèle**: {getattr(self.llm, 'model', 'pixtral-12b-2409')}\n" - rapport += f"- **Température**: {getattr(self.llm, 'temperature', 0.3)}\n" - - return rapport - -def charger_config(): - """ - Charge la configuration depuis config.json. - - Returns: - Configuration chargée - """ - config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") - - if not os.path.exists(config_path): - logger.warning(f"Fichier de configuration non trouvé: {config_path}") - return {"llm": {"api_key": None}} - - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as e: - logger.error(f"Erreur lors du chargement de la configuration: {str(e)}") - return {"llm": {"api_key": None}} - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Analyse une image en fonction du contexte du ticket.") - parser.add_argument("--image", "-i", required=True, help="Chemin vers l'image à analyser") - parser.add_argument("--contexte", "-c", help="Chemin vers un fichier contenant le contexte du ticket") - parser.add_argument("--ticket-info", "-t", help="Chemin vers un fichier ticket_info.json pour extraire le contexte") - parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le rapport Markdown (par défaut: _analyse.md)") - parser.add_argument("--format", "-f", choices=["json", "md", "both"], default="both", - help="Format de sortie (json, md, both)") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Vérifier que l'image existe - if not os.path.exists(args.image): - logger.error(f"Image non trouvée: {args.image}") - sys.exit(1) - - # Charger le contexte si disponible - contexte = None - - if args.contexte and os.path.exists(args.contexte): - try: - with open(args.contexte, 'r', encoding='utf-8') as f: - contexte = f.read() - logger.info(f"Contexte chargé depuis {args.contexte}") - except Exception as e: - logger.warning(f"Impossible de charger le contexte depuis {args.contexte}: {str(e)}") - - # Extraire le contexte depuis ticket_info.json si disponible - if not contexte and args.ticket_info and os.path.exists(args.ticket_info): - try: - with open(args.ticket_info, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - if isinstance(ticket_info, dict): - contexte = f""" - TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')} - - DESCRIPTION: - {ticket_info.get('description', 'Aucune description')} - """ - logger.info(f"Contexte extrait depuis {args.ticket_info}") - except Exception as e: - logger.warning(f"Impossible de charger le contexte depuis {args.ticket_info}: {str(e)}") - - # Déterminer les chemins de sortie - if not args.output: - output_base = os.path.splitext(args.image)[0] - output_md = f"{output_base}_analyse.md" - output_json = f"{output_base}_analyse.json" - else: - output_base = os.path.splitext(args.output)[0] - output_md = f"{output_base}.md" - output_json = f"{output_base}.json" - - # Charger la configuration - config = charger_config() - api_key = config.get("llm", {}).get("api_key") - - # Initialiser l'analyseur d'image - analyzer = ImageAnalyzer(api_key=api_key) - - try: - # Analyser l'image - resultat = analyzer.analyser_image(args.image, contexte) - - if not resultat.get("success", False): - logger.error(f"Échec de l'analyse: {resultat.get('erreur', 'Erreur inconnue')}") - sys.exit(1) - - # Générer le rapport Markdown - rapport_md = analyzer.generer_rapport_markdown(resultat) - - # Sauvegarder les résultats selon le format demandé - if args.format in ["json", "both"]: - with open(output_json, 'w', encoding='utf-8') as f: - json.dump(resultat, f, indent=2, ensure_ascii=False) - logger.info(f"Résultat JSON sauvegardé: {output_json}") - - if args.format in ["md", "both"]: - with open(output_md, 'w', encoding='utf-8') as f: - f.write(rapport_md) - logger.info(f"Rapport Markdown sauvegardé: {output_md}") - - # Afficher un résumé - print("\nRésumé de l'analyse:") - print(f"Image: {os.path.basename(args.image)}") - - if "type_image" in resultat: - print(f"Type d'image: {resultat['type_image']}") - - if "interface" in resultat: - print(f"Interface: {resultat['interface']}") - - if "probleme" in resultat: - print(f"Problème: {resultat['probleme']}") - - if args.format in ["json", "both"]: - print(f"Résultat JSON: {output_json}") - - if args.format in ["md", "both"]: - print(f"Rapport Markdown: {output_md}") - - except Exception as e: - logger.error(f"Erreur lors de l'analyse: {str(e)}") - import traceback - logger.debug(f"Détails: {traceback.format_exc()}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/analyze_ticket.py b/scripts/analyze_ticket.py deleted file mode 100644 index 8f266ba..0000000 --- a/scripts/analyze_ticket.py +++ /dev/null @@ -1,444 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script d'analyse de ticket pour extraire les informations essentielles -et générer un rapport d'analyse complet. -""" - -import os -import sys -import json -import argparse -import logging -from typing import Dict, List, Any, Optional - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("analyze_ticket.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("analyze_ticket") - -try: - from llm import Mistral -except ImportError: - logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.") - sys.exit(1) - -class TicketAnalyzer: - """ - Agent d'analyse de ticket qui extrait les informations pertinentes. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent d'analyse de ticket. - - Args: - api_key: Clé API pour le LLM - """ - self.llm = Mistral(api_key=api_key) - self.llm.set_model("mistral-medium") - self.llm.set_temperature(0.3) - self.llm.set_max_tokens(1000) - - # Définir le prompt système par défaut - self.system_prompt = """ - Tu es un expert en analyse de tickets de support technique. - - Ton objectif est d'analyser un ticket de support pour: - 1. Identifier le problème principal - 2. Résumer la solution (si présente) - 3. Extraire les informations clés - 4. Catégoriser le problème et sa gravité - 5. Évaluer la qualité de la résolution - - Utilise un ton professionnel et factuel. - Concentre-toi uniquement sur les informations pertinentes. - Ne spécule pas au-delà de ce qui est présent dans les données. - - Si une information n'est pas disponible, indique-le clairement. - """ - - self.historique = [] - - def ajouter_historique(self, action: str, entree: str, resultat: str) -> None: - """ - Ajoute une entrée à l'historique des actions. - - Args: - action: Type d'action effectuée - entree: Entrée de l'action - resultat: Résultat de l'action - """ - self.historique.append({ - "action": action, - "entree": entree, - "resultat": resultat - }) - - def analyser_ticket(self, messages: List[Dict[str, Any]], infos_images: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """ - Analyse un ticket à partir de ses messages et informations d'images. - - Args: - messages: Liste des messages du ticket - infos_images: Informations sur les images analysées (optionnel) - - Returns: - Résultats de l'analyse du ticket - """ - if not messages: - logger.warning("Aucun message à analyser") - return { - "success": False, - "erreur": "Aucun message à analyser" - } - - logger.info(f"Analyse de ticket avec {len(messages)} messages") - - # Extraire les informations du ticket depuis le premier message (contexte) - ticket_info = {} - if messages and messages[0].get("role") == "system" and messages[0].get("type") == "contexte": - ticket_info = { - "id": messages[0].get("id", ""), - "code": messages[0].get("code", ""), - "name": messages[0].get("name", ""), - "description": messages[0].get("description", ""), - "date_create": messages[0].get("date_create", "") - } - - # Retirer le message de contexte pour l'analyse - actual_messages = messages[1:] - else: - actual_messages = messages - - # Préparer le prompt pour l'analyse - prompt = f""" - Analyse ce ticket de support: - - TICKET: {ticket_info.get('code', 'N/A')} - {ticket_info.get('name', 'Sans titre')} - DATE: {ticket_info.get('date_create', 'Inconnue')} - - DESCRIPTION: - {ticket_info.get('description', 'Aucune description')} - - MESSAGES: - """ - - # Ajouter les messages - for i, msg in enumerate(actual_messages): - role = msg.get("role", "Inconnu") - date = msg.get("date", "") - body = msg.get("body", "") - - prompt += f"\n--- MESSAGE {i+1} ({role}, {date}) ---\n{body}\n" - - # Ajouter les informations sur les images si disponibles - if infos_images: - prompt += "\n\nIMAGES ANALYSÉES:\n" - - for image_path, analyse in infos_images.get("analyses", {}).items(): - if analyse.get("pertinente", False): - prompt += f"- {image_path}: {analyse.get('description', 'Pas de description')} ({analyse.get('type_image', 'type inconnu')})\n" - - # Demander une analyse structurée - prompt += """ - - Fais une analyse complète et structurée du ticket avec les sections suivantes: - - 1. PROBLÈME: Résume clairement le problème principal en 1-2 phrases - 2. CATÉGORIE: Catégorise le problème (bug, demande de fonctionnalité, question, etc.) - 3. GRAVITÉ: Évalue la gravité (Critique, Élevée, Moyenne, Faible) - 4. SOLUTION: Résume la solution fournie ou indique qu'aucune solution n'a été fournie - 5. EFFICACITÉ: Évalue si la solution a résolu le problème (Résolue, Partiellement résolue, Non résolue, Inconnue) - 6. RÉSUMÉ: Fournis un résumé complet de l'incident en 3-5 phrases - 7. POINTS CLÉS: Liste les 3-5 points les plus importants à retenir de ce ticket - - Réponds en format Markdown bien structuré. - """ - - try: - # Effectuer l'analyse avec le LLM - resultat = self.llm.generate_text(prompt, system_prompt=self.system_prompt) - self.ajouter_historique("analyze_ticket", f"{len(messages)} messages", "Analyse effectuée") - - # Extraire le contenu - analyse_texte = resultat.get("content", "") - - # Extraire les différentes sections - sections = {} - - current_section = None - current_content = [] - - for line in analyse_texte.split("\n"): - # Détecter les en-têtes de section - if line.startswith("# "): - if current_section: - sections[current_section] = "\n".join(current_content).strip() - current_section = line[2:].strip().lower() - current_content = [] - elif line.startswith("## "): - if current_section: - sections[current_section] = "\n".join(current_content).strip() - current_section = line[3:].strip().lower() - current_content = [] - elif ":" in line and not "://" in line and not current_section: - # Cas des lignes "SECTION: contenu" sans formatage Markdown - parts = line.split(":", 1) - if len(parts) == 2 and parts[0].strip().upper() == parts[0].strip(): - current_section = parts[0].strip().lower() - current_content = [parts[1].strip()] - else: - if current_section: - current_content.append(line) - else: - if current_section: - current_content.append(line) - - # Ajouter la dernière section - if current_section: - sections[current_section] = "\n".join(current_content).strip() - - # Si on n'a pas pu extraire les sections, utiliser tout le texte - if not sections: - sections = { - "analyse_complete": analyse_texte - } - - # Créer le résultat final - resultat_analyse = { - "success": True, - "ticket_info": ticket_info, - "sections": sections, - "analyse_brute": analyse_texte, - "nb_messages": len(actual_messages) - } - - logger.info("Analyse de ticket terminée avec succès") - return resultat_analyse - - except Exception as e: - erreur = f"Erreur lors de l'analyse du ticket: {str(e)}" - logger.error(erreur) - return { - "success": False, - "erreur": erreur - } - - def generer_rapport_markdown(self, analyse: Dict[str, Any]) -> str: - """ - Génère un rapport Markdown à partir de l'analyse du ticket. - - Args: - analyse: Résultat de l'analyse du ticket - - Returns: - Rapport au format Markdown - """ - if not analyse.get("success", False): - return f"# Échec de l'analyse\n\nErreur: {analyse.get('erreur', 'Inconnue')}" - - ticket_info = analyse.get("ticket_info", {}) - sections = analyse.get("sections", {}) - - # En-tête du rapport - rapport = f"# Rapport d'analyse de ticket\n\n" - rapport += f"**Ticket**: {ticket_info.get('code', 'N/A')} - {ticket_info.get('name', 'Sans titre')}\n" - rapport += f"**Date**: {ticket_info.get('date_create', 'Inconnue')}\n" - rapport += f"**Messages analysés**: {analyse.get('nb_messages', 0)}\n\n" - - # Récupérer les sections principales - problem = sections.get("problème", sections.get("probleme", "")) - category = sections.get("catégorie", sections.get("categorie", "")) - severity = sections.get("gravité", sections.get("gravite", "")) - solution = sections.get("solution", "") - efficacy = sections.get("efficacité", sections.get("efficacite", "")) - summary = sections.get("résumé", sections.get("resume", "")) - key_points = sections.get("points clés", sections.get("points cles", "")) - - # Ajouter les sections au rapport - if problem: - rapport += f"## Problème\n\n{problem}\n\n" - - if category or severity: - rapport += "## Classification\n\n" - if category: - rapport += f"**Catégorie**: {category}\n\n" - if severity: - rapport += f"**Gravité**: {severity}\n\n" - - if solution: - rapport += f"## Solution\n\n{solution}\n\n" - - if efficacy: - rapport += f"**Efficacité**: {efficacy}\n\n" - - if summary: - rapport += f"## Résumé\n\n{summary}\n\n" - - if key_points: - rapport += f"## Points clés\n\n{key_points}\n\n" - - # Ajouter les autres sections qui n'auraient pas été traitées - for name, content in sections.items(): - if name not in ["problème", "probleme", "catégorie", "categorie", - "gravité", "gravite", "solution", "efficacité", - "efficacite", "résumé", "resume", "points clés", - "points cles", "analyse_complete"]: - rapport += f"## {name.title()}\n\n{content}\n\n" - - # Ajouter le rapport complet si on n'a pas pu extraire les sections - if "analyse_complete" in sections and len(sections) == 1: - rapport += f"## Analyse complète\n\n{sections['analyse_complete']}\n\n" - - # Ajouter les paramètres de l'analyse - rapport += "## Paramètres de l'analyse\n\n" - rapport += f"- **Modèle**: {self.llm.get_model()}\n" - rapport += f"- **Température**: {self.llm.get_temperature()}\n" - - return rapport - -def charger_config(): - """ - Charge la configuration depuis config.json. - - Returns: - Configuration chargée - """ - config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") - - if not os.path.exists(config_path): - logger.warning(f"Fichier de configuration non trouvé: {config_path}") - return {"llm": {"api_key": None}} - - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as e: - logger.error(f"Erreur lors du chargement de la configuration: {str(e)}") - return {"llm": {"api_key": None}} - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Analyse un ticket de support.") - parser.add_argument("--messages", "-m", required=True, help="Chemin vers le fichier messages.json") - parser.add_argument("--images-rapport", "-i", help="Chemin vers le rapport d'analyse d'images (filter_report.json)") - parser.add_argument("--output", "-o", help="Répertoire de sortie pour les rapports") - parser.add_argument("--format", "-f", choices=["json", "md", "both"], default="both", - help="Format de sortie (json, md, both)") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Vérifier que le fichier messages existe - if not os.path.exists(args.messages): - logger.error(f"Fichier de messages non trouvé: {args.messages}") - sys.exit(1) - - # Charger les messages - try: - with open(args.messages, 'r', encoding='utf-8') as f: - messages = json.load(f) - - if not isinstance(messages, list): - logger.error(f"Format de fichier messages.json invalide. Une liste est attendue.") - sys.exit(1) - except Exception as e: - logger.error(f"Erreur lors du chargement des messages: {str(e)}") - sys.exit(1) - - # Charger les informations sur les images si disponibles - images_info = None - if args.images_rapport and os.path.exists(args.images_rapport): - try: - with open(args.images_rapport, 'r', encoding='utf-8') as f: - images_info = json.load(f) - logger.info(f"Informations sur les images chargées: {args.images_rapport}") - except Exception as e: - logger.warning(f"Impossible de charger les informations sur les images: {str(e)}") - - # Déterminer le répertoire de sortie - output_dir = args.output - if not output_dir: - # Par défaut, utiliser le même répertoire que le fichier messages - output_dir = os.path.dirname(args.messages) - if not output_dir: - output_dir = "." - - # Créer le répertoire de sortie s'il n'existe pas - rapport_dir = os.path.join(output_dir, "rapport") - os.makedirs(rapport_dir, exist_ok=True) - - # Charger la configuration - config = charger_config() - api_key = config.get("llm", {}).get("api_key") - - # Initialiser l'analyseur de ticket - analyzer = TicketAnalyzer(api_key=api_key) - - try: - # Analyser le ticket - resultat = analyzer.analyser_ticket(messages, images_info) - - if not resultat.get("success", False): - logger.error(f"Échec de l'analyse: {resultat.get('erreur', 'Erreur inconnue')}") - sys.exit(1) - - # Générer le rapport Markdown - rapport_md = analyzer.generer_rapport_markdown(resultat) - - # Sauvegarder les résultats selon le format demandé - if args.format in ["json", "both"]: - json_path = os.path.join(rapport_dir, "ticket_analysis.json") - with open(json_path, 'w', encoding='utf-8') as f: - json.dump(resultat, f, indent=2, ensure_ascii=False) - logger.info(f"Rapport JSON sauvegardé: {json_path}") - - if args.format in ["md", "both"]: - md_path = os.path.join(rapport_dir, "ticket_analysis.md") - with open(md_path, 'w', encoding='utf-8') as f: - f.write(rapport_md) - logger.info(f"Rapport Markdown sauvegardé: {md_path}") - - # Afficher un résumé - print("\nRésumé de l'analyse:") - print(f"Ticket: {resultat.get('ticket_info', {}).get('code', 'N/A')} - {resultat.get('ticket_info', {}).get('name', 'Sans titre')}") - print(f"Messages analysés: {resultat.get('nb_messages', 0)}") - print(f"Sections extraites: {len(resultat.get('sections', {}))}") - - # Afficher un extrait du problème et de la solution - sections = resultat.get("sections", {}) - probleme = sections.get("problème", sections.get("probleme", "")) - solution = sections.get("solution", "") - - if probleme: - probleme_court = probleme[:150] + "..." if len(probleme) > 150 else probleme - print(f"\nProblème: {probleme_court}") - - if solution: - solution_court = solution[:150] + "..." if len(solution) > 150 else solution - print(f"\nSolution: {solution_court}") - - print(f"\nRappport complet sauvegardé dans: {rapport_dir}") - - except Exception as e: - logger.error(f"Erreur lors de l'analyse: {str(e)}") - import traceback - logger.debug(f"Détails: {traceback.format_exc()}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/extract_question_reponse.py b/scripts/extract_question_reponse.py deleted file mode 100644 index 1196a14..0000000 --- a/scripts/extract_question_reponse.py +++ /dev/null @@ -1,534 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script d'extraction des questions et réponses d'un ticket. -Génère un tableau Markdown avec les questions et réponses identifiées. -""" - -import os -import sys -import json -import argparse -import logging -import re -from typing import Dict, List, Any, Optional - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("extract_qr.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("extract_qr") - -try: - from llm import Mistral -except ImportError: - logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.") - sys.exit(1) - -class QuestionReponseExtractor: - """ - Agent d'extraction des questions et réponses d'un ticket. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent d'extraction de questions-réponses. - - Args: - api_key: Clé API pour le LLM - """ - self.llm = Mistral(api_key=api_key) - - # Configurer le LLM - try: - self.llm.model = "mistral-medium" - self.llm.temperature = 0.3 - self.llm.max_tokens = 2000 - except Exception as e: - logger.warning(f"Impossible de configurer le modèle: {e}") - - # Définir le prompt système par défaut - self.system_prompt = """ - Tu es un expert en analyse de conversations de support technique. - - Votre mission est d'identifier avec précision: - 1. Le rôle de chaque intervenant (client ou support technique) - 2. La nature de chaque message (question, réponse, information additionnelle) - 3. Le contenu essentiel de chaque message en éliminant les formules de politesse, - signatures, mentions légales et autres éléments non pertinents - - Pour l'identification client/support: - - Support: Signatures avec noms d'entreprise fournissant le logiciel, domaines email - comme @cbao.fr, @odoo.com, mentions "support technique", etc. - - Client: Utilisateurs finaux qui signalent des problèmes ou posent des questions - - Pour la classification en question/réponse: - - Questions: Demandes explicites (avec "?"), demandes implicites de résolution - de problèmes, descriptions de bugs ou dysfonctionnements - - Réponses: Explications techniques, solutions proposées, instructions fournies - par le support - - Concentre-toi uniquement sur le contenu technique utile en ignorant tous les - éléments superflus qui n'apportent pas d'information sur le problème ou sa solution. - """ - - self.historique = [] - - def ajouter_historique(self, action: str, entree: str, resultat: str) -> None: - """ - Ajoute une entrée à l'historique des actions. - - Args: - action: Type d'action effectuée - entree: Entrée de l'action - resultat: Résultat de l'action - """ - self.historique.append({ - "action": action, - "entree": entree, - "resultat": resultat - }) - - def nettoyer_contenu(self, texte: str) -> str: - """ - Nettoie le contenu en supprimant signatures, mentions légales, etc. - - Args: - texte: Texte brut à nettoyer - - Returns: - Texte nettoyé des éléments non pertinents - """ - # Si l'entrée n'est pas une chaîne, convertir en chaîne ou retourner vide - if not isinstance(texte, str): - if texte is None: - return "" - try: - texte = str(texte) - except: - return "" - - # Détecter et supprimer les balises HTML avec regex robuste - try: - # Première passe - balises standard - texte_nettoye = re.sub(r']*>', ' ', texte, flags=re.IGNORECASE) - - # Deuxième passe - balises restantes, y compris les mal formées - texte_nettoye = re.sub(r'<[^>]*>', ' ', texte_nettoye) - - # Troisième passe pour les balises qui pourraient avoir échappé - texte_nettoye = re.sub(r'<[^>]*$', ' ', texte_nettoye) # Balises incomplètes à la fin - except Exception as e: - logger.warning(f"Erreur lors du nettoyage HTML: {e}") - texte_nettoye = texte - - # Remplacer les références aux images - texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye) - texte_nettoye = re.sub(r']+>', '[Image]', texte_nettoye, flags=re.IGNORECASE) - - # Supprimer les éléments courants non pertinents - patterns_a_supprimer = [ - r'Cordialement,[\s\S]*?$', - r'Bien cordialement,[\s\S]*?$', - r'Bonne réception[\s\S]*?$', - r'À votre disposition[\s\S]*?$', - r'Support technique[\s\S]*?$', - r'L\'objectif du Support Technique[\s\S]*?$', - r'Notre service est ouvert[\s\S]*?$', - r'Dès réception[\s\S]*?$', - r'Confidentialité[\s\S]*?$', - r'Ce message électronique[\s\S]*?$', - r'Droit à la déconnexion[\s\S]*?$', - r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$', - r'tél\s*:\s*[\d\s\+]+', - r'mobile\s*:\s*[\d\s\+]+', - r'www\.[^\s]+\.[a-z]{2,3}', - r'\*{10,}.*?\*{10,}', # Lignes de séparation avec astérisques - r'----.*?----', # Lignes de séparation avec tirets - ] - - for pattern in patterns_a_supprimer: - texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE) - - # Supprimer les lignes multiples vides et espaces multiples - texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye) - texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye) - - # Convertir les entités HTML - html_entities = { - ' ': ' ', '<': '<', '>': '>', '&': '&', - '"': '"', ''': "'", '€': '€', '©': '©', - '®': '®', 'é': 'é', 'è': 'è', 'à': 'à', - 'ç': 'ç', 'ê': 'ê', 'â': 'â', 'î': 'î', - 'ô': 'ô', 'û': 'û' - } - - for entity, char in html_entities.items(): - texte_nettoye = texte_nettoye.replace(entity, char) - - return texte_nettoye.strip() - - def detecter_role(self, message: Dict[str, Any]) -> str: - """ - Détecte si un message provient du client ou du support. - - Args: - message: Dictionnaire contenant les informations du message - - Returns: - "Client" ou "Support" - """ - # Vérifier le champ 'role' s'il existe déjà - if "role" in message and message["role"] in ["Client", "Support"]: - return message["role"] - - # Indices de support dans l'email - domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"] - indices_nom_support = ["support", "cbao", "technique", "odoo"] - - email = message.get("email_from", "").lower() - # Nettoyer le format "Nom " - if "<" in email and ">" in email: - match = re.search(r'<([^>]+)>', email) - if match: - email = match.group(1).lower() - - # Vérifier le domaine email - if any(domaine in email for domaine in domaines_support): - return "Support" - - # Vérifier le nom d'auteur - auteur = "" - if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1: - auteur = str(message["author_id"][1]).lower() - elif "auteur" in message: - auteur = str(message["auteur"]).lower() - - if any(indice in auteur for indice in indices_nom_support): - return "Support" - - # Par défaut, considérer comme client - return "Client" - - def extraire_questions_reponses(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]: - """ - Extrait les questions et réponses d'une liste de messages. - - Args: - messages: Liste des messages du ticket - - Returns: - Dictionnaire avec les questions et réponses extraites - """ - if not messages: - logger.warning("Aucun message à analyser") - return { - "success": False, - "erreur": "Aucun message à analyser", - "paires_qr": [] - } - - logger.info(f"Extraction des questions et réponses de {len(messages)} messages") - - # Préparation des messages - messages_prepares = [] - for msg in messages: - # Nettoyer le contenu - contenu = msg.get("body", "") or msg.get("contenu", "") - contenu_nettoye = self.nettoyer_contenu(contenu) - - # Détecter le rôle - role = self.detecter_role(msg) - - # Ajouter le message préparé si non vide après nettoyage - if contenu_nettoye.strip(): - messages_prepares.append({ - "id": msg.get("id", "") or msg.get("ID", ""), - "date": msg.get("date", ""), - "role": role, - "body": contenu_nettoye - }) - - # S'il n'y a pas assez de messages pour une conversation - if len(messages_prepares) < 2: - logger.warning("Pas assez de messages pour une conversation") - return { - "success": True, - "paires_qr": [], - "nb_questions": 0, - "nb_reponses": 0 - } - - # Trier par date si disponible - messages_prepares.sort(key=lambda x: x.get("date", "")) - - # Préparer l'entrée pour le LLM - messages_for_llm = [] - for i, msg in enumerate(messages_prepares): - messages_for_llm.append({ - "numero": i + 1, - "role": msg.get("role", "Inconnu"), - "date": msg.get("date", ""), - "contenu": msg.get("body", "") - }) - - # Préparer le prompt pour extraire les paires Q/R - prompt = """ - Analyse la conversation suivante et identifie toutes les paires de questions et réponses. - - Pour chaque message: - 1. Identifie s'il s'agit d'une question, d'une réponse ou d'une information. - 2. Extrais le contenu essentiel en ignorant les formules de politesse et les signatures. - - Ensuite, forme des paires de questions-réponses en associant chaque question avec sa réponse correspondante. - - Réponds en utilisant la structure suivante: - - ``` - MESSAGE 1: - - Rôle: [Client/Support] - - Type: [Question/Réponse/Information] - - Contenu essentiel: [Contenu essentiel du message] - - MESSAGE 2: - ... - - PAIRE 1: - - Question (Client): [Question posée] - - Réponse (Support): [Réponse donnée] - - PAIRE 2: - ... - ``` - - Si une question n'a pas de réponse, indique-le. - """ - - try: - # Appeler le LLM pour l'analyse - from json import dumps - resultat = self.llm.chat_completion([ - {"role": "system", "content": self.system_prompt}, - {"role": "user", "content": prompt + "\n\nConversation:\n" + dumps(messages_for_llm, indent=2)} - ]) - - contenu = resultat.get("choices", [{}])[0].get("message", {}).get("content", "") - self.ajouter_historique("analyze_messages", f"{len(messages)} messages", "Analyse effectuée") - - # Traiter la réponse pour extraire les messages analysés - messages_analyses = [] - pattern_messages = r"MESSAGE (\d+):\s*- Rôle: (Client|Support)\s*- Type: (Question|Réponse|Information)\s*- Contenu essentiel: (.*?)(?=MESSAGE \d+:|PAIRE \d+:|$)" - for match in re.finditer(pattern_messages, contenu, re.DOTALL): - num = int(match.group(1)) - role = match.group(2) - type_msg = match.group(3) - contenu_essentiel = match.group(4).strip() - - # Trouver le message correspondant - msg_idx = num - 1 - msg_id = "" - msg_date = "" - - if 0 <= msg_idx < len(messages_for_llm): - original_idx = messages_for_llm[msg_idx]["numero"] - 1 - if 0 <= original_idx < len(messages_prepares): - msg_id = messages_prepares[original_idx].get("id", "") - msg_date = messages_prepares[original_idx].get("date", "") - - messages_analyses.append({ - "id": msg_id, - "date": msg_date, - "role": role, - "type": type_msg, - "contenu": contenu_essentiel - }) - - # Extraire les paires QR - paires_qr = [] - pattern_paires = r"PAIRE (\d+):\s*- Question \((Client|Support)\): (.*?)(?:\s*- Réponse \((Client|Support)\): (.*?))?(?=PAIRE \d+:|$)" - for match in re.finditer(pattern_paires, contenu, re.DOTALL): - num = match.group(1) - q_role = match.group(2) - question = match.group(3).strip() - r_role = match.group(4) if match.group(4) else "" - reponse = match.group(5).strip() if match.group(5) else "" - - paires_qr.append({ - "numero": num, - "question": { - "role": q_role, - "contenu": question - }, - "reponse": { - "role": r_role, - "contenu": reponse - } if reponse else None - }) - - return { - "success": True, - "messages_analyses": messages_analyses, - "paires_qr": paires_qr, - "nb_questions": len(paires_qr), - "nb_reponses": sum(1 for p in paires_qr if p.get("reponse")) - } - - except Exception as e: - erreur = f"Erreur lors de l'extraction des questions et réponses: {str(e)}" - logger.error(erreur) - return { - "success": False, - "erreur": erreur, - "paires_qr": [] - } - - def generer_tableau_markdown(self, paires_qr: List[Dict[str, Any]]) -> str: - """ - Génère un tableau Markdown avec les questions et réponses. - - Args: - paires_qr: Liste de paires question/réponse - - Returns: - Tableau Markdown formaté - """ - # Créer le tableau - markdown = ["# Analyse des Questions et Réponses\n"] - markdown.append("| Question | Réponse |") - markdown.append("|---------|---------|") - - if not paires_qr: - # Si aucune paire n'a été trouvée, laisser le tableau vide - pass - else: - for paire in paires_qr: - question = paire.get("question", {}) - reponse = paire.get("reponse", {}) - - q_role = question.get("role", "Client") - q_contenu = question.get("contenu", "") - - if reponse: - r_role = reponse.get("role", "Support") - r_contenu = reponse.get("contenu", "") - - markdown.append(f"| **{q_role}**: {q_contenu} | **{r_role}**: {r_contenu} |") - else: - markdown.append(f"| **{q_role}**: {q_contenu} | *Pas de réponse* |") - - # Ajouter les informations sur les paramètres LLM utilisés - markdown.append("\n## Paramètres LLM utilisés\n") - - markdown.append(f"- **Type de LLM**: Mistral") - markdown.append(f"- **Modèle**: {getattr(self.llm, 'model', 'mistral-medium')}") - markdown.append(f"- **Température**: {getattr(self.llm, 'temperature', 0.3)}") - markdown.append(f"- **Tokens max**: {getattr(self.llm, 'max_tokens', 2000)}") - - return "\n".join(markdown) - -def charger_config(): - """ - Charge la configuration depuis config.json. - - Returns: - Configuration chargée - """ - config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") - - if not os.path.exists(config_path): - logger.warning(f"Fichier de configuration non trouvé: {config_path}") - return {"llm": {"api_key": None}} - - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as e: - logger.error(f"Erreur lors du chargement de la configuration: {str(e)}") - return {"llm": {"api_key": None}} - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Extrait les questions et réponses d'un ticket de support.") - parser.add_argument("--messages", "-m", required=True, help="Chemin vers le fichier messages.json") - parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le tableau Markdown (par défaut: /questions_reponses.md)") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Vérifier que le fichier messages existe - if not os.path.exists(args.messages): - logger.error(f"Fichier de messages non trouvé: {args.messages}") - sys.exit(1) - - # Charger les messages - try: - with open(args.messages, 'r', encoding='utf-8') as f: - messages = json.load(f) - - if not isinstance(messages, list): - logger.error(f"Format de fichier messages.json invalide. Une liste est attendue.") - sys.exit(1) - except Exception as e: - logger.error(f"Erreur lors du chargement des messages: {str(e)}") - sys.exit(1) - - # Déterminer le chemin de sortie - output_path = args.output - if not output_path: - # Par défaut, utiliser le même répertoire que le fichier messages - output_dir = os.path.dirname(args.messages) - if not output_dir: - output_dir = "." - output_path = os.path.join(output_dir, "questions_reponses.md") - - # Charger la configuration - config = charger_config() - api_key = config.get("llm", {}).get("api_key") - - # Initialiser l'extracteur de questions-réponses - extractor = QuestionReponseExtractor(api_key=api_key) - - try: - # Extraire les questions et réponses - resultats = extractor.extraire_questions_reponses(messages) - - if not resultats.get("success", False): - logger.error(f"Échec de l'extraction: {resultats.get('erreur', 'Erreur inconnue')}") - sys.exit(1) - - # Générer le tableau Markdown - tableau_md = extractor.generer_tableau_markdown(resultats.get("paires_qr", [])) - - # Sauvegarder le tableau - with open(output_path, 'w', encoding='utf-8') as f: - f.write(tableau_md) - - logger.info(f"Tableau Markdown sauvegardé: {output_path}") - - # Afficher un résumé - print("\nRésumé de l'extraction:") - print(f"Messages analysés: {len(messages)}") - print(f"Questions extraites: {resultats.get('nb_questions', 0)}") - print(f"Réponses extraites: {resultats.get('nb_reponses', 0)}") - print(f"Tableau Markdown sauvegardé: {output_path}") - - except Exception as e: - logger.error(f"Erreur lors de l'extraction: {str(e)}") - import traceback - logger.debug(f"Détails: {traceback.format_exc()}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/extract_ticket.py b/scripts/extract_ticket.py deleted file mode 100644 index e89d6a4..0000000 --- a/scripts/extract_ticket.py +++ /dev/null @@ -1,504 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script d'extraction et de prétraitement des tickets. -Nettoie les données et sépare les pièces jointes des messages. -""" - -import os -import sys -import json -import re -import shutil -import argparse -import unicodedata -from typing import Dict, List, Any, Optional -from bs4 import BeautifulSoup -import logging - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("extract_ticket.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("extract_ticket") - -def nettoyer_html(texte: str) -> str: - """ - Nettoie le contenu HTML en utilisant BeautifulSoup. - - Args: - texte: Texte HTML à nettoyer - - Returns: - Texte nettoyé - """ - if not texte: - return "" - - if not isinstance(texte, str): - try: - texte = str(texte) - except Exception as e: - logger.warning(f"Impossible de convertir en texte: {e}") - return "" - - # Utiliser BeautifulSoup pour le nettoyage - try: - soup = BeautifulSoup(texte, 'html.parser') - texte_nettoye = soup.get_text(separator=' ') - except Exception as e: - logger.warning(f"Erreur lors du nettoyage HTML avec BeautifulSoup: {e}") - # Fallback à regex si BeautifulSoup échoue - try: - texte_nettoye = re.sub(r'<[^>]+>', ' ', texte) - except Exception as e: - logger.warning(f"Erreur lors du nettoyage HTML avec regex: {e}") - texte_nettoye = texte - - # Remplacer les références aux images - texte_nettoye = re.sub(r'\[Image:[^\]]+\]', '[Image]', texte_nettoye) - - # Supprimer les éléments courants non pertinents - patterns_a_supprimer = [ - r'Cordialement,[\s\S]*?$', - r'Bien cordialement,[\s\S]*?$', - r'Bonne réception[\s\S]*?$', - r'À votre disposition[\s\S]*?$', - r'Support technique[\s\S]*?$', - r'L\'objectif du Support Technique[\s\S]*?$', - r'Notre service est ouvert[\s\S]*?$', - r'Dès réception[\s\S]*?$', - r'Confidentialité[\s\S]*?$', - r'Ce message électronique[\s\S]*?$', - r'Droit à la déconnexion[\s\S]*?$', - r'Afin d\'assurer une meilleure traçabilité[\s\S]*?$', - r'tél\s*:\s*[\d\s\+]+', - r'mobile\s*:\s*[\d\s\+]+', - r'www\.[^\s]+\.[a-z]{2,3}', - ] - - for pattern in patterns_a_supprimer: - texte_nettoye = re.sub(pattern, '', texte_nettoye, flags=re.IGNORECASE) - - # Supprimer les lignes multiples vides - texte_nettoye = re.sub(r'\n\s*\n', '\n', texte_nettoye) - - # Supprimer les espaces multiples - texte_nettoye = re.sub(r'\s+', ' ', texte_nettoye) - - # Normaliser les caractères accentués - texte_nettoye = normaliser_accents(texte_nettoye) - - return texte_nettoye.strip() - -def normaliser_accents(texte: str) -> str: - """ - Normalise les caractères accentués pour éviter les problèmes d'encodage. - - Args: - texte: Texte à normaliser - - Returns: - Texte avec caractères accentués normalisés - """ - if not isinstance(texte, str): - if texte is None: - return "" - try: - texte = str(texte) - except: - return "" - - # Convertir les caractères spéciaux HTML - special_chars = { - 'á': 'á', 'é': 'é', 'í': 'í', 'ó': 'ó', 'ú': 'ú', - 'Á': 'Á', 'É': 'É', 'Í': 'Í', 'Ó': 'Ó', 'Ú': 'Ú', - 'à': 'à', 'è': 'è', 'ì': 'ì', 'ò': 'ò', 'ù': 'ù', - 'À': 'À', 'È': 'È', 'Ì': 'Ì', 'Ò': 'Ò', 'Ù': 'Ù', - 'â': 'â', 'ê': 'ê', 'î': 'î', 'ô': 'ô', 'û': 'û', - 'Â': 'Â', 'Ê': 'Ê', 'Î': 'Î', 'Ô': 'Ô', 'Û': 'Û', - 'ã': 'ã', '&etilde;': 'ẽ', 'ĩ': 'ĩ', 'õ': 'õ', 'ũ': 'ũ', - 'Ã': 'Ã', '&Etilde;': 'Ẽ', 'Ĩ': 'Ĩ', 'Õ': 'Õ', 'Ũ': 'Ũ', - 'ä': 'ä', 'ë': 'ë', 'ï': 'ï', 'ö': 'ö', 'ü': 'ü', - 'Ä': 'Ä', 'Ë': 'Ë', 'Ï': 'Ï', 'Ö': 'Ö', 'Ü': 'Ü', - 'ç': 'ç', 'Ç': 'Ç', 'ñ': 'ñ', 'Ñ': 'Ñ', - ' ': ' ', '<': '<', '>': '>', '&': '&', '"': '"', ''': "'", - '€': '€', '©': '©', '®': '®', '™': '™' - } - - for html, char in special_chars.items(): - texte = texte.replace(html, char) - - # Normaliser les caractères composés - return unicodedata.normalize('NFC', texte) - -def detecter_role(message: Dict[str, Any]) -> str: - """ - Détecte si un message provient du client ou du support. - - Args: - message: Dictionnaire contenant les informations du message - - Returns: - "Client" ou "Support" - """ - # Vérifier le champ 'role' s'il existe déjà - if "role" in message and message["role"] in ["Client", "Support"]: - return message["role"] - - # Indices de support dans l'email - domaines_support = ["@cbao.fr", "@odoo.com", "support@", "ticket.support"] - indices_nom_support = ["support", "cbao", "technique", "odoo"] - - email = message.get("email_from", "").lower() - # Nettoyer le format "Nom " - if "<" in email and ">" in email: - match = re.search(r'<([^>]+)>', email) - if match: - email = match.group(1).lower() - - # Vérifier le domaine email - if any(domaine in email for domaine in domaines_support): - return "Support" - - # Vérifier le nom d'auteur - auteur = "" - if "author_id" in message and isinstance(message["author_id"], list) and len(message["author_id"]) > 1: - auteur = str(message["author_id"][1]).lower() - elif "auteur" in message: - auteur = str(message["auteur"]).lower() - - if any(indice in auteur for indice in indices_nom_support): - return "Support" - - # Par défaut, considérer comme client - return "Client" - -def pretraiter_ticket(input_dir: str, output_dir: str) -> Dict[str, Any]: - """ - Prétraite les données d'un ticket et les sépare en fichiers distincts. - - Args: - input_dir: Répertoire contenant les données brutes du ticket - output_dir: Répertoire où sauvegarder les données prétraitées - - Returns: - Rapport de prétraitement avec les fichiers générés - """ - logger.info(f"Prétraitement du ticket: {input_dir} -> {output_dir}") - - # Créer le répertoire de sortie s'il n'existe pas - os.makedirs(output_dir, exist_ok=True) - - # Créer les sous-répertoires - attachments_dir = os.path.join(output_dir, "attachments") - os.makedirs(attachments_dir, exist_ok=True) - - # Chemins des fichiers d'entrée - ticket_info_path = os.path.join(input_dir, "ticket_info.json") - messages_path = os.path.join(input_dir, "messages.json") - messages_backup_path = os.path.join(input_dir, "messages.json.backup") - - # Rapport de prétraitement - rapport = { - "ticket_id": os.path.basename(input_dir), - "fichiers_generes": [], - "erreurs": [] - } - - # Prétraiter ticket_info.json - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - # Nettoyer la description - if isinstance(ticket_info, dict) and "description" in ticket_info: - ticket_info["description"] = nettoyer_html(ticket_info["description"]) - - # Sauvegarder dans le répertoire de sortie - output_ticket_info_path = os.path.join(output_dir, "ticket_info.json") - with open(output_ticket_info_path, 'w', encoding='utf-8') as f: - json.dump(ticket_info, f, indent=2, ensure_ascii=False) - - rapport["fichiers_generes"].append("ticket_info.json") - logger.info(f"Ticket info prétraité et sauvegardé: {output_ticket_info_path}") - - except Exception as e: - erreur = f"Erreur lors du prétraitement de ticket_info.json: {str(e)}" - rapport["erreurs"].append(erreur) - logger.error(erreur) - else: - erreur = f"Fichier ticket_info.json non trouvé dans {input_dir}" - rapport["erreurs"].append(erreur) - logger.warning(erreur) - - # Prétraiter messages.json - messages_content = None - - # D'abord essayer messages.json - if os.path.exists(messages_path): - try: - with open(messages_path, 'r', encoding='utf-8') as f: - messages_content = f.read() - except Exception as e: - logger.warning(f"Impossible de lire messages.json: {str(e)}") - - # Si messages.json est vide ou corrompu, essayer la sauvegarde - if not messages_content and os.path.exists(messages_backup_path): - try: - with open(messages_backup_path, 'r', encoding='utf-8') as f: - messages_content = f.read() - logger.info("Utilisation de messages.json.backup comme source") - except Exception as e: - erreur = f"Impossible de lire messages.json.backup: {str(e)}" - rapport["erreurs"].append(erreur) - logger.error(erreur) - - # Traiter les messages si nous avons un contenu valide - if messages_content: - try: - messages = json.loads(messages_content) - - # Créer une version améliorée des messages - processed_messages = [] - - # Déterminer le code du ticket à partir du nom du répertoire - ticket_code = os.path.basename(input_dir) - if ticket_code.startswith("ticket_"): - ticket_code = ticket_code[7:] # Extraire le code sans "ticket_" - - # Extraire les informations du ticket si disponibles - ticket_info_dict = {} - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info_dict = json.load(f) - except Exception: - pass - - # Créer le message de contexte avec les informations du ticket - ticket_name = ticket_info_dict.get("name", f"Ticket {ticket_code}") - ticket_description = ticket_info_dict.get("description", "") - ticket_date = ticket_info_dict.get("create_date", "") - - # Nettoyer les informations du ticket - ticket_name = normaliser_accents(ticket_name) - ticket_description = nettoyer_html(ticket_description) - - ticket_context = { - "id": "ticket_info", - "name": ticket_name, - "code": ticket_code, - "description": ticket_description, - "date_create": ticket_date, - "role": "system", - "type": "contexte", - "body": f"TICKET {ticket_code}: {ticket_name}.\n\nDESCRIPTION: {ticket_description or 'Aucune description disponible.'}" - } - processed_messages.append(ticket_context) - - # Prétraiter chaque message - attachments_info = [] - valid_messages = 0 - - for msg in messages: - if not isinstance(msg, dict): - continue - - # Ignorer les messages vides - body = msg.get("body", "") - if not body or not isinstance(body, str): - continue - - # Détecter le rôle - role = detecter_role(msg) - message_type = "Question" if role == "Client" else "Réponse" - - # Nettoyer le contenu - contenu_nettoye = nettoyer_html(body) - if not contenu_nettoye: - continue - - # Normaliser les champs textuels - email_from = normaliser_accents(msg.get("email_from", "")) - subject = normaliser_accents(msg.get("subject", "")) - - # Gérer l'identifiant du message - msg_id = msg.get("id", f"msg_{valid_messages+1}") - if not isinstance(msg_id, str): - try: - msg_id = str(msg_id) - except: - msg_id = f"msg_{valid_messages+1}" - - # Récupérer les autres champs de manière sécurisée - author_id = msg.get("author_id", [0, ""]) - if not isinstance(author_id, list): - author_id = [0, ""] - - date = msg.get("date", "") - if not isinstance(date, str): - try: - date = str(date) - except: - date = "" - - # Traiter les pièces jointes si présentes - if "attachments" in msg and isinstance(msg["attachments"], list): - for attachment in msg["attachments"]: - if not isinstance(attachment, dict): - continue - - attachment_data = attachment.get("datas") - attachment_name = attachment.get("name", "") - attachment_type = attachment.get("mimetype", "") - - if attachment_data and attachment_name: - # Générer un nom de fichier unique - attachment_id = attachment.get("id", len(attachments_info) + 1) - safe_name = f"{attachment_id}_{attachment_name}" - file_path = os.path.join(attachments_dir, safe_name) - - # Traiter différemment selon le type de pièce jointe - if attachment_type.startswith("image/"): - try: - # Sauvegarder l'image - import base64 - with open(file_path, 'wb') as f: - f.write(base64.b64decode(attachment_data)) - - # Ajouter l'information à la liste des pièces jointes - attachments_info.append({ - "id": attachment_id, - "name": attachment_name, - "mimetype": attachment_type, - "message_id": msg_id, - "date": date, - "file_path": file_path - }) - - logger.info(f"Pièce jointe sauvegardée: {file_path}") - except Exception as e: - logger.warning(f"Erreur lors de la sauvegarde de la pièce jointe {attachment_name}: {str(e)}") - - # Créer le message transformé - processed_message = { - "id": msg_id, - "author_id": author_id, - "role": role, - "type": message_type, - "date": date, - "email_from": email_from, - "subject": subject, - "body": contenu_nettoye - } - - processed_messages.append(processed_message) - valid_messages += 1 - - # Trier par date (sauf le premier message qui est le contexte) - try: - processed_messages[1:] = sorted(processed_messages[1:], key=lambda x: x.get("date", "")) - except Exception as e: - logger.warning(f"Impossible de trier les messages par date: {e}") - - # Sauvegarder les messages prétraités - output_messages_path = os.path.join(output_dir, "messages.json") - with open(output_messages_path, 'w', encoding='utf-8') as f: - json.dump(processed_messages, f, indent=2, ensure_ascii=False) - - rapport["fichiers_generes"].append("messages.json") - logger.info(f"Messages prétraités et sauvegardés: {output_messages_path} ({valid_messages} messages)") - - # Sauvegarder les informations sur les pièces jointes - if attachments_info: - output_attachments_info_path = os.path.join(output_dir, "attachments_info.json") - with open(output_attachments_info_path, 'w', encoding='utf-8') as f: - json.dump(attachments_info, f, indent=2, ensure_ascii=False) - - rapport["fichiers_generes"].append("attachments_info.json") - rapport["nb_attachments"] = len(attachments_info) - logger.info(f"Informations sur les pièces jointes sauvegardées: {output_attachments_info_path} ({len(attachments_info)} pièces jointes)") - - except Exception as e: - erreur = f"Erreur lors du prétraitement des messages: {str(e)}" - rapport["erreurs"].append(erreur) - logger.error(erreur) - else: - erreur = "Aucun fichier messages.json ou messages.json.backup trouvé ou lisible" - rapport["erreurs"].append(erreur) - logger.error(erreur) - - # Sauvegarder le rapport de prétraitement - rapport_path = os.path.join(output_dir, "pretraitement_rapport.json") - with open(rapport_path, 'w', encoding='utf-8') as f: - json.dump(rapport, f, indent=2, ensure_ascii=False) - - logger.info(f"Rapport de prétraitement sauvegardé: {rapport_path}") - - return rapport - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Prétraite les données d'un ticket.") - parser.add_argument("input_dir", help="Répertoire contenant les données brutes du ticket") - parser.add_argument("--output-dir", help="Répertoire où sauvegarder les données prétraitées (par défaut: _processed)") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Déterminer le répertoire de sortie - input_dir = args.input_dir - if not os.path.isdir(input_dir): - logger.error(f"Le répertoire d'entrée n'existe pas: {input_dir}") - sys.exit(1) - - output_dir = args.output_dir - if not output_dir: - # Par défaut, ajouter "_processed" au nom du répertoire d'entrée - if input_dir.endswith("/"): - input_dir = input_dir[:-1] - output_dir = input_dir + "_processed" - - # Prétraiter le ticket - try: - rapport = pretraiter_ticket(input_dir, output_dir) - - # Afficher un résumé - print("\nRésumé du prétraitement:") - print(f"Ticket: {rapport['ticket_id']}") - print(f"Fichiers générés: {len(rapport['fichiers_generes'])}") - for fichier in rapport['fichiers_generes']: - print(f" - {fichier}") - - if "nb_attachments" in rapport: - print(f"Pièces jointes: {rapport['nb_attachments']}") - - if rapport['erreurs']: - print(f"Erreurs: {len(rapport['erreurs'])}") - for erreur in rapport['erreurs']: - print(f" - {erreur}") - else: - print("Aucune erreur") - - print(f"\nPrétraitement terminé. Données sauvegardées dans: {output_dir}") - - except Exception as e: - logger.error(f"Erreur lors du prétraitement: {str(e)}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/filter_images.py b/scripts/filter_images.py deleted file mode 100644 index f0b783d..0000000 --- a/scripts/filter_images.py +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script de filtrage des images pertinentes dans un ticket. -Identifie et sépare les images utiles des images non pertinentes. -""" - -import os -import sys -import json -import argparse -import logging -import datetime -import re -from typing import Dict, List, Any, Optional - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("filter_images.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("filter_images") - -try: - from llm import Pixtral # Importer le modèle d'analyse d'images -except ImportError: - logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.") - sys.exit(1) - -class ImageFilterAgent: - """ - Agent responsable du filtrage des images pertinentes. - """ - def __init__(self, api_key: Optional[str] = None): - """ - Initialise l'agent de filtrage d'images. - - Args: - api_key: Clé API pour le modèle de vision - """ - self.llm = Pixtral(api_key=api_key) - # Configurer le modèle de vision - self.llm.set_model("pixtral-12b-2409") - self.llm.set_temperature(0.2) - self.llm.set_max_tokens(500) - self.historique = [] - - def ajouter_historique(self, action: str, entree: str, resultat: str) -> None: - """ - Ajoute une entrée à l'historique des actions. - - Args: - action: Type d'action effectuée - entree: Entrée de l'action - resultat: Résultat de l'action - """ - self.historique.append({ - "action": action, - "entree": entree, - "resultat": resultat, - "timestamp": datetime.datetime.now().isoformat() - }) - - def est_image_pertinente(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]: - """ - Détermine si une image est pertinente pour l'analyse du ticket. - - Args: - image_path: Chemin vers l'image à analyser - contexte: Contexte du ticket pour aider à l'analyse - - Returns: - Résultat de l'analyse avec la pertinence et le type d'image - """ - if not os.path.exists(image_path): - logger.warning(f"Image introuvable: {image_path}") - return { - "pertinente": False, - "type_image": "inconnue", - "description": "Image introuvable", - "erreur": "Fichier non trouvé" - } - - try: - # Préparer le prompt pour l'analyse - prompt_base = """ - Tu es un expert en analyse d'images techniques pour le support informatique. - - Analyse cette image et détermine si elle est pertinente pour comprendre le problème technique décrit. - - Une image pertinente est: - - Une capture d'écran montrant une interface, une erreur, ou une fonctionnalité logicielle - - Un schéma technique ou une illustration du problème - - Une photo d'un équipement ou d'un matériel en lien avec le ticket - - Une image non pertinente est: - - Un logo, une signature, ou une image décorative - - Une photo de personne sans lien avec le problème technique - - Une image générique non liée au contexte du ticket - - Réponds au format JSON avec les champs suivants: - - pertinente: boolean (true/false) - - type_image: string (capture_ecran, schéma, photo_équipement, logo, autre) - - description: string (description courte de ce que l'image montre) - - confiance: integer (niveau de confiance de 0 à 100) - - justification: string (pourquoi cette image est pertinente ou non) - """ - - # Ajouter le contexte si disponible - if contexte: - prompt_base += f"\n\nContexte du ticket:\n{contexte}" - - # Analyser l'image avec le modèle de vision - try: - resultat = self.llm.analyze_image(image_path, prompt_base) - self.ajouter_historique("analyze_image", os.path.basename(image_path), "Analyse effectuée") - except Exception as e: - logger.error(f"Erreur lors de l'appel au modèle de vision: {str(e)}") - return { - "pertinente": False, - "type_image": "inconnue", - "description": "Erreur d'analyse", - "erreur": str(e) - } - - # Extraire le JSON de la réponse - json_match = re.search(r'```json\s*(.*?)\s*```', resultat.get("content", ""), re.DOTALL) - - if json_match: - try: - analyse = json.loads(json_match.group(1)) - return analyse - except Exception as e: - logger.error(f"Erreur lors du parsing JSON: {str(e)}") - else: - # Essayer de trouver un JSON sans les backticks - try: - # Nettoyer la réponse pour essayer d'extraire le JSON - content = resultat.get("content", "") - # Trouver les accolades ouvrantes et fermantes - start_idx = content.find('{') - end_idx = content.rfind('}') - - if start_idx != -1 and end_idx != -1: - json_str = content[start_idx:end_idx+1] - analyse = json.loads(json_str) - return analyse - except Exception as e: - logger.error(f"Impossible d'extraire le JSON de la réponse: {str(e)}") - - # Si on n'a pas pu extraire le JSON, analyser manuellement la réponse - content = resultat.get("content", "").lower() - est_pertinente = "pertinente" in content and not "non pertinente" in content - - return { - "pertinente": est_pertinente, - "type_image": "inconnue" if not est_pertinente else "autre", - "description": "Analyse non structurée disponible", - "confiance": 50, - "reponse_brute": resultat.get("content", "") - } - - except Exception as e: - logger.error(f"Erreur lors de l'analyse de l'image {image_path}: {str(e)}") - return { - "pertinente": False, - "type_image": "inconnue", - "description": "Erreur lors de l'analyse", - "erreur": str(e) - } - - def filtrer_images(self, images_paths: List[str], contexte: Optional[str] = None) -> Dict[str, Any]: - """ - Analyse et filtre une liste d'images pour identifier celles qui sont pertinentes. - - Args: - images_paths: Liste des chemins vers les images à analyser - contexte: Contexte du ticket pour aider à l'analyse - - Returns: - Rapport de filtrage avec les images pertinentes et non pertinentes - """ - logger.info(f"Filtrage de {len(images_paths)} images...") - - resultats = { - "images_pertinentes": [], - "images_non_pertinentes": [], - "erreurs": [], - "analyses": {} - } - - for image_path in images_paths: - logger.info(f"Analyse de l'image: {os.path.basename(image_path)}") - - # Vérifier que le fichier existe et est une image - if not os.path.exists(image_path): - logger.warning(f"Image introuvable: {image_path}") - resultats["erreurs"].append(f"Image introuvable: {image_path}") - continue - - # Vérifier l'extension pour s'assurer que c'est une image - _, extension = os.path.splitext(image_path) - if extension.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']: - logger.warning(f"Format de fichier non supporté: {image_path}") - resultats["erreurs"].append(f"Format de fichier non supporté: {image_path}") - continue - - # Analyser l'image - analyse = self.est_image_pertinente(image_path, contexte) - - # Ajouter à la catégorie appropriée - if "erreur" in analyse: - resultats["erreurs"].append(f"Erreur d'analyse pour {os.path.basename(image_path)}: {analyse['erreur']}") - resultats["analyses"][os.path.basename(image_path)] = analyse - elif analyse.get("pertinente", False): - resultats["images_pertinentes"].append(image_path) - resultats["analyses"][os.path.basename(image_path)] = analyse - logger.info(f"Image pertinente: {os.path.basename(image_path)} - {analyse.get('type_image', 'type inconnu')}") - else: - resultats["images_non_pertinentes"].append(image_path) - resultats["analyses"][os.path.basename(image_path)] = analyse - logger.info(f"Image non pertinente: {os.path.basename(image_path)}") - - logger.info(f"Filtrage terminé. {len(resultats['images_pertinentes'])} images pertinentes, {len(resultats['images_non_pertinentes'])} non pertinentes, {len(resultats['erreurs'])} erreurs.") - return resultats - -def charger_config(): - """ - Charge la configuration depuis config.json. - - Returns: - Configuration chargée - """ - config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") - - if not os.path.exists(config_path): - logger.warning(f"Fichier de configuration non trouvé: {config_path}") - return {"llm": {"api_key": None}} - - try: - with open(config_path, 'r', encoding='utf-8') as f: - config = json.load(f) - return config - except Exception as e: - logger.error(f"Erreur lors du chargement de la configuration: {str(e)}") - return {"llm": {"api_key": None}} - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Filtre les images pertinentes dans un ticket.") - parser.add_argument("--images", "-i", nargs="+", help="Liste des chemins vers les images à filtrer") - parser.add_argument("--contexte", "-c", help="Fichier JSON contenant le contexte du ticket") - parser.add_argument("--dossier-ticket", "-d", help="Dossier du ticket contenant attachments/ et ticket_info.json") - parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le rapport JSON (par défaut: filter_report.json)") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Charger la configuration - config = charger_config() - api_key = config.get("llm", {}).get("api_key") - - # Initialiser l'agent de filtrage - agent = ImageFilterAgent(api_key=api_key) - - # Déterminer les images à filtrer - images_paths = [] - contexte = None - - if args.images: - images_paths = args.images - elif args.dossier_ticket: - # Chercher dans le dossier attachments/ du ticket - attachments_dir = os.path.join(args.dossier_ticket, "attachments") - if os.path.isdir(attachments_dir): - # Récupérer toutes les images du dossier - for filename in os.listdir(attachments_dir): - file_path = os.path.join(attachments_dir, filename) - _, extension = os.path.splitext(filename) - if os.path.isfile(file_path) and extension.lower() in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']: - images_paths.append(file_path) - - # Charger le contexte du ticket - ticket_info_path = os.path.join(args.dossier_ticket, "ticket_info.json") - if os.path.exists(ticket_info_path): - try: - with open(ticket_info_path, 'r', encoding='utf-8') as f: - ticket_info = json.load(f) - - # Créer un contexte à partir des informations du ticket - contexte = f""" - TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')} - - DESCRIPTION: - {ticket_info.get('description', 'Aucune description')} - """ - except Exception as e: - logger.warning(f"Impossible de charger le contexte depuis ticket_info.json: {str(e)}") - - # Charger le contexte explicite si fourni - if args.contexte: - try: - with open(args.contexte, 'r', encoding='utf-8') as f: - if args.contexte.endswith('.json'): - contexte_data = json.load(f) - if isinstance(contexte_data, dict): - contexte = f""" - TICKET: {contexte_data.get('code', 'Inconnu')} - {contexte_data.get('name', 'Sans titre')} - - DESCRIPTION: - {contexte_data.get('description', 'Aucune description')} - """ - else: - contexte = str(contexte_data) - else: - contexte = f.read() - except Exception as e: - logger.warning(f"Impossible de charger le contexte depuis {args.contexte}: {str(e)}") - - # Vérifier que nous avons des images à traiter - if not images_paths: - logger.error("Aucune image à filtrer. Utilisez --images ou --dossier-ticket pour spécifier les images.") - sys.exit(1) - - # Filtrer les images - try: - resultats = agent.filtrer_images(images_paths, contexte) - - # Déterminer le chemin de sortie - output_path = args.output - if not output_path: - if args.dossier_ticket: - output_path = os.path.join(args.dossier_ticket, "filter_report.json") - else: - output_path = "filter_report.json" - - # Sauvegarder le rapport - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(resultats, f, indent=2, ensure_ascii=False) - - logger.info(f"Rapport de filtrage sauvegardé: {output_path}") - - # Afficher un résumé - print("\nRésumé du filtrage:") - print(f"Images analysées: {len(images_paths)}") - print(f"Images pertinentes: {len(resultats['images_pertinentes'])}") - print(f"Images non pertinentes: {len(resultats['images_non_pertinentes'])}") - print(f"Erreurs: {len(resultats['erreurs'])}") - - if resultats['images_pertinentes']: - print("\nImages pertinentes:") - for img in resultats['images_pertinentes']: - img_name = os.path.basename(img) - img_type = resultats['analyses'].get(img_name, {}).get('type_image', 'type inconnu') - print(f" - {img_name} ({img_type})") - - if resultats['erreurs']: - print("\nErreurs:") - for err in resultats['erreurs']: - print(f" - {err}") - - print(f"\nRapport complet sauvegardé dans: {output_path}") - - except Exception as e: - logger.error(f"Erreur lors du filtrage des images: {str(e)}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/processus_complet.py b/scripts/processus_complet.py deleted file mode 100644 index 8bc77f5..0000000 --- a/scripts/processus_complet.py +++ /dev/null @@ -1,383 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Script principal d'orchestration du processus d'analyse de tickets. -Ce script permet d'exécuter toutes les étapes du traitement ou des étapes individuelles. -""" - -import os -import sys -import json -import argparse -import subprocess -import logging -from typing import Dict, List, Any, Optional -import shutil - -# Configuration du logger -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler("processus_complet.log"), - logging.StreamHandler() - ] -) -logger = logging.getLogger("processus_complet") - -def executer_commande(commande: List[str], description: str) -> bool: - """ - Exécute une commande système et gère les erreurs. - - Args: - commande: Liste des éléments de la commande à exécuter - description: Description de la commande pour le journal - - Returns: - True si la commande s'est exécutée avec succès, False sinon - """ - try: - logger.info(f"Exécution: {description}") - logger.debug(f"Commande: {' '.join(commande)}") - - resultat = subprocess.run(commande, check=True, capture_output=True, text=True) - - logger.info(f"Succès: {description}") - logger.debug(f"Sortie: {resultat.stdout}") - - return True - except subprocess.CalledProcessError as e: - logger.error(f"Échec: {description}") - logger.error(f"Code de sortie: {e.returncode}") - logger.error(f"Erreur: {e.stderr}") - return False - except Exception as e: - logger.error(f"Erreur lors de l'exécution de la commande: {str(e)}") - return False - -def etape_extraction(ticket_dir: str, output_dir: str) -> bool: - """ - Exécute l'étape d'extraction des données du ticket. - - Args: - ticket_dir: Répertoire contenant les données brutes du ticket - output_dir: Répertoire où sauvegarder les données extraites - - Returns: - True si l'extraction a réussi, False sinon - """ - script_path = os.path.join("scripts", "extract_ticket.py") - - if not os.path.exists(script_path): - logger.error(f"Script d'extraction non trouvé: {script_path}") - return False - - commande = [ - sys.executable, - script_path, - ticket_dir, - "--output-dir", output_dir, - "--verbose" - ] - - return executer_commande(commande, "Extraction des données du ticket") - -def etape_filtrage_images(ticket_dir: str) -> bool: - """ - Exécute l'étape de filtrage des images pertinentes. - - Args: - ticket_dir: Répertoire contenant les données du ticket - - Returns: - True si le filtrage a réussi, False sinon - """ - script_path = os.path.join("scripts", "filter_images.py") - - if not os.path.exists(script_path): - logger.error(f"Script de filtrage d'images non trouvé: {script_path}") - return False - - commande = [ - sys.executable, - script_path, - "--dossier-ticket", ticket_dir, - "--output", os.path.join(ticket_dir, "filter_report.json"), - "--verbose" - ] - - return executer_commande(commande, "Filtrage des images pertinentes") - -def etape_analyse_images(ticket_dir: str, rapport_filtrage: str) -> bool: - """ - Exécute l'étape d'analyse des images pertinentes. - - Args: - ticket_dir: Répertoire contenant les données du ticket - rapport_filtrage: Chemin vers le rapport de filtrage d'images - - Returns: - True si l'analyse a réussi, False sinon - """ - script_path = os.path.join("scripts", "analyze_image_contexte.py") - ticket_info_path = os.path.join(ticket_dir, "ticket_info.json") - - if not os.path.exists(script_path): - logger.error(f"Script d'analyse d'images non trouvé: {script_path}") - return False - - # Charger le rapport de filtrage - try: - with open(rapport_filtrage, 'r', encoding='utf-8') as f: - filtre_data = json.load(f) - - images_pertinentes = filtre_data.get("images_pertinentes", []) - if not images_pertinentes: - logger.info("Aucune image pertinente à analyser") - return True - except Exception as e: - logger.error(f"Erreur lors du chargement du rapport de filtrage: {str(e)}") - return False - - # Créer le répertoire pour les rapports d'analyse d'images - images_analyses_dir = os.path.join(ticket_dir, "images_analyses") - os.makedirs(images_analyses_dir, exist_ok=True) - - # Analyser chaque image pertinente - succes = True - for image_path in images_pertinentes: - image_name = os.path.basename(image_path) - output_base = os.path.join(images_analyses_dir, image_name) - - commande = [ - sys.executable, - script_path, - "--image", image_path, - "--ticket-info", ticket_info_path, - "--output", output_base + "_analyse", - "--verbose" - ] - - if not executer_commande(commande, f"Analyse de l'image {image_name}"): - succes = False - - return succes - -def etape_analyse_ticket(ticket_dir: str, rapport_filtrage: str) -> bool: - """ - Exécute l'étape d'analyse du contenu du ticket. - - Args: - ticket_dir: Répertoire contenant les données du ticket - rapport_filtrage: Chemin vers le rapport de filtrage d'images - - Returns: - True si l'analyse a réussi, False sinon - """ - script_path = os.path.join("scripts", "analyze_ticket.py") - messages_path = os.path.join(ticket_dir, "messages.json") - - if not os.path.exists(script_path): - logger.error(f"Script d'analyse de ticket non trouvé: {script_path}") - return False - - commande = [ - sys.executable, - script_path, - "--messages", messages_path, - "--images-rapport", rapport_filtrage, - "--output", ticket_dir, - "--verbose" - ] - - return executer_commande(commande, "Analyse du contenu du ticket") - -def etape_questions_reponses(ticket_dir: str) -> bool: - """ - Exécute l'étape d'extraction des questions et réponses. - - Args: - ticket_dir: Répertoire contenant les données du ticket - - Returns: - True si l'extraction a réussi, False sinon - """ - script_path = os.path.join("scripts", "extract_question_reponse.py") - messages_path = os.path.join(ticket_dir, "messages.json") - output_path = os.path.join(ticket_dir, "questions_reponses.md") - - if not os.path.exists(script_path): - logger.error(f"Script d'extraction des questions-réponses non trouvé: {script_path}") - return False - - commande = [ - sys.executable, - script_path, - "--messages", messages_path, - "--output", output_path, - "--verbose" - ] - - return executer_commande(commande, "Extraction des questions et réponses") - -def processus_complet(ticket_code: str, dossier_source: str = None, dossier_sortie: str = None) -> bool: - """ - Exécute le processus complet d'analyse d'un ticket. - - Args: - ticket_code: Code du ticket à analyser - dossier_source: Dossier contenant les tickets bruts (par défaut: output/) - dossier_sortie: Dossier où sauvegarder les résultats (par défaut: output_processed/) - - Returns: - True si le processus s'est exécuté avec succès, False sinon - """ - # Définir les dossiers par défaut si non spécifiés - if dossier_source is None: - dossier_source = "output" - - if dossier_sortie is None: - dossier_sortie = "output_processed" - - # Construire les chemins - ticket_dir_source = os.path.join(dossier_source, f"ticket_{ticket_code}") - ticket_dir_sortie = os.path.join(dossier_sortie, f"ticket_{ticket_code}") - - # Vérifier que le dossier source existe - if not os.path.exists(ticket_dir_source): - logger.error(f"Dossier source non trouvé: {ticket_dir_source}") - return False - - # Créer le dossier de sortie s'il n'existe pas - os.makedirs(ticket_dir_sortie, exist_ok=True) - - # 1. Extraction des données - if not etape_extraction(ticket_dir_source, ticket_dir_sortie): - logger.error("Échec de l'étape d'extraction") - return False - - # 2. Filtrage des images - if not etape_filtrage_images(ticket_dir_sortie): - logger.error("Échec de l'étape de filtrage des images") - return False - - # 3. Analyse des images pertinentes - rapport_filtrage = os.path.join(ticket_dir_sortie, "filter_report.json") - if not etape_analyse_images(ticket_dir_sortie, rapport_filtrage): - logger.error("Échec de l'étape d'analyse des images") - return False - - # 4. Analyse du contenu du ticket - if not etape_analyse_ticket(ticket_dir_sortie, rapport_filtrage): - logger.error("Échec de l'étape d'analyse du ticket") - return False - - # 5. Extraction des questions et réponses - if not etape_questions_reponses(ticket_dir_sortie): - logger.error("Échec de l'étape d'extraction des questions et réponses") - return False - - logger.info(f"Processus complet terminé avec succès pour le ticket {ticket_code}") - logger.info(f"Résultats disponibles dans: {ticket_dir_sortie}") - - return True - -def main(): - """ - Point d'entrée du script. - """ - parser = argparse.ArgumentParser(description="Exécute le processus d'analyse de tickets de support.") - parser.add_argument("--ticket", "-t", required=True, help="Code du ticket à analyser (ex: T0167)") - parser.add_argument("--source", "-s", help="Dossier source contenant les tickets bruts (par défaut: output/)") - parser.add_argument("--output", "-o", help="Dossier de sortie pour les résultats (par défaut: output_processed/)") - parser.add_argument("--etapes", "-e", choices=["extraction", "filtrage", "analyse_images", "analyse_ticket", "questions_reponses", "tout"], - default="tout", help="Étapes à exécuter") - parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") - - args = parser.parse_args() - - # Configurer le niveau de log - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Récupérer le code du ticket - ticket_code = args.ticket - if ticket_code.startswith("ticket_"): - ticket_code = ticket_code[7:] - - # Définir les dossiers source et sortie - dossier_source = args.source or "output" - dossier_sortie = args.output or "output_processed" - - # Construire les chemins - ticket_dir_source = os.path.join(dossier_source, f"ticket_{ticket_code}") - ticket_dir_sortie = os.path.join(dossier_sortie, f"ticket_{ticket_code}") - - # Vérifier que le dossier source existe - if not os.path.exists(ticket_dir_source): - logger.error(f"Dossier source non trouvé: {ticket_dir_source}") - sys.exit(1) - - # Exécuter les étapes demandées - if args.etapes == "tout": - if processus_complet(ticket_code, dossier_source, dossier_sortie): - print(f"Processus complet terminé avec succès pour le ticket {ticket_code}") - print(f"Résultats disponibles dans: {ticket_dir_sortie}") - else: - print(f"Échec du processus pour le ticket {ticket_code}") - sys.exit(1) - else: - # Créer le dossier de sortie s'il n'existe pas - os.makedirs(ticket_dir_sortie, exist_ok=True) - - # Exécuter l'étape spécifique - if args.etapes == "extraction": - if etape_extraction(ticket_dir_source, ticket_dir_sortie): - print("Étape d'extraction terminée avec succès") - else: - print("Échec de l'étape d'extraction") - sys.exit(1) - - elif args.etapes == "filtrage": - if etape_filtrage_images(ticket_dir_sortie): - print("Étape de filtrage des images terminée avec succès") - else: - print("Échec de l'étape de filtrage des images") - sys.exit(1) - - elif args.etapes == "analyse_images": - rapport_filtrage = os.path.join(ticket_dir_sortie, "filter_report.json") - if not os.path.exists(rapport_filtrage): - logger.error(f"Rapport de filtrage non trouvé: {rapport_filtrage}") - print("Veuillez d'abord exécuter l'étape de filtrage des images") - sys.exit(1) - - if etape_analyse_images(ticket_dir_sortie, rapport_filtrage): - print("Étape d'analyse des images terminée avec succès") - else: - print("Échec de l'étape d'analyse des images") - sys.exit(1) - - elif args.etapes == "analyse_ticket": - rapport_filtrage = os.path.join(ticket_dir_sortie, "filter_report.json") - if not os.path.exists(rapport_filtrage): - logger.error(f"Rapport de filtrage non trouvé: {rapport_filtrage}") - print("Veuillez d'abord exécuter l'étape de filtrage des images") - sys.exit(1) - - if etape_analyse_ticket(ticket_dir_sortie, rapport_filtrage): - print("Étape d'analyse du ticket terminée avec succès") - else: - print("Échec de l'étape d'analyse du ticket") - sys.exit(1) - - elif args.etapes == "questions_reponses": - if etape_questions_reponses(ticket_dir_sortie): - print("Étape d'extraction des questions et réponses terminée avec succès") - else: - print("Échec de l'étape d'extraction des questions et réponses") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index c7b84af..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from .ticket_analyzer import TicketAnalyzer -from .ticket_manager import TicketManager - -__all__ = ['TicketAnalyzer', 'TicketManager'] \ No newline at end of file diff --git a/utils/__pycache__/__init__.cpython-312.pyc b/utils/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 4122d57..0000000 Binary files a/utils/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/utils/__pycache__/ticket_analyzer.cpython-312.pyc b/utils/__pycache__/ticket_analyzer.cpython-312.pyc deleted file mode 100644 index 5ab4dfd..0000000 Binary files a/utils/__pycache__/ticket_analyzer.cpython-312.pyc and /dev/null differ diff --git a/utils/__pycache__/ticket_manager.cpython-312.pyc b/utils/__pycache__/ticket_manager.cpython-312.pyc deleted file mode 100644 index a611b3a..0000000 Binary files a/utils/__pycache__/ticket_manager.cpython-312.pyc and /dev/null differ diff --git a/utils/ticket_analyzer.py b/utils/ticket_analyzer.py deleted file mode 100644 index a6a1840..0000000 --- a/utils/ticket_analyzer.py +++ /dev/null @@ -1,358 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Utilitaire pour l'analyse de tickets de support utilisant les agents spécialisés. -""" - -import os -import json -from datetime import datetime -from typing import Dict, List, Any, Optional - -from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse -from post_process import normaliser_accents, corriger_markdown_accents - -class TicketAnalyzer: - """ - Classe utilitaire pour analyser des tickets de support. - Coordonne le travail des différents agents spécialisés. - """ - - def __init__(self, api_key: Optional[str] = None, llm_params: Optional[Dict[str, Any]] = None): - """ - Initialise l'analyseur de tickets. - - Args: - api_key: Clé API pour les modèles LLM - llm_params: Paramètres globaux pour les LLM - """ - self.api_key = api_key - self.llm_params = llm_params or {} - - # Initialisation des agents - self.agent_filtre = AgentFiltreImages(api_key=api_key) - self.agent_analyse = AgentAnalyseImage(api_key=api_key) - self.agent_qr = AgentQuestionReponse(api_key=api_key) - - # Appliquer les paramètres globaux - self._appliquer_parametres_globaux() - - # Journal d'analyse - self.entries = [] - - def _appliquer_parametres_globaux(self) -> None: - """ - Applique les paramètres globaux à tous les agents. - """ - if not self.llm_params: - return - - print(f"Application des paramètres globaux LLM: {self.llm_params}") - self.agent_filtre.appliquer_parametres_globaux(self.llm_params) - self.agent_analyse.appliquer_parametres_globaux(self.llm_params) - self.agent_qr.appliquer_parametres_globaux(self.llm_params) - - def filtrer_images(self, images_paths: List[str]) -> List[str]: - """ - Filtre les images pour ne conserver que celles pertinentes. - - Args: - images_paths: Liste des chemins d'images à filtrer - - Returns: - Liste des chemins d'images pertinentes - """ - images_pertinentes = [] - - for image_path in images_paths: - # Enregistrer le début de l'action - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - entry = { - "timestamp": timestamp, - "action": "filter_image", - "agent": self.agent_filtre.nom, - "llm": {"model": self.agent_filtre.llm.model}, - "parametres_llm": self.agent_filtre.obtenir_parametres_llm(), - "image_path": image_path - } - - # Exécuter le filtrage d'image - resultat = self.agent_filtre.executer(image_path) - - # Ajouter le résultat à l'entrée - entry["response"] = resultat - - # Ajouter au journal - self.entries.append(entry) - - # Conserver l'image si pertinente - if resultat.get("pertinente", False): - images_pertinentes.append(image_path) - - return images_pertinentes - - def analyser_images(self, images_paths: List[str], contexte_ticket: Optional[str] = None) -> List[Dict[str, Any]]: - """ - Analyse les images pertinentes en détail. - - Args: - images_paths: Liste des chemins d'images à analyser - contexte_ticket: Contexte du ticket pour une analyse plus pertinente - - Returns: - Liste des résultats d'analyse - """ - resultats_analyses = [] - - for image_path in images_paths: - # Enregistrer le début de l'action - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - entry = { - "timestamp": timestamp, - "action": "analyze_image", - "agent": self.agent_analyse.nom, - "llm": {"model": self.agent_analyse.llm.model}, - "parametres_llm": self.agent_analyse.obtenir_parametres_llm(), - "image_path": image_path - } - - # Exécuter l'analyse d'image - resultat = self.agent_analyse.executer(image_path, contexte_ticket) - - # Ajouter le résultat à l'entrée - entry["response"] = resultat.get("content", "") - if "usage" in resultat: - entry["tokens"] = resultat["usage"] - - # Ajouter au journal - self.entries.append(entry) - - # Ajouter aux résultats - if "error" not in resultat: - resultats_analyses.append({ - "image_path": image_path, - "analyse": resultat.get("content", ""), - "usage": resultat.get("usage", {}), - "parametres_llm": self.agent_analyse.generer_rapport_parametres() - }) - - return resultats_analyses - - def extraire_questions_reponses(self, messages: List[Dict[str, Any]], output_path: Optional[str] = None) -> Dict[str, Any]: - """ - Extrait les questions et réponses des messages du ticket. - - Args: - messages: Liste des messages du ticket - output_path: Chemin où sauvegarder le tableau Markdown (optionnel) - - Returns: - Résultat de l'extraction avec tableau Markdown - """ - # Enregistrer le début de l'action - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - entry = { - "timestamp": timestamp, - "action": "extract_questions_reponses", - "agent": self.agent_qr.nom, - "llm": {"model": self.agent_qr.llm.model}, - "parametres_llm": self.agent_qr.obtenir_parametres_llm() - } - - # Exécuter l'extraction des questions/réponses - resultat = self.agent_qr.executer(messages, output_path) - - # Ajouter le résultat à l'entrée - entry["response"] = resultat - - # Ajouter au journal - self.entries.append(entry) - - return resultat - - def generer_rapport(self, output_path: str) -> Dict[str, str]: - """ - Génère un rapport complet d'analyse au format JSON et Markdown. - - Args: - output_path: Chemin où sauvegarder le rapport - - Returns: - Dictionnaire avec les chemins des fichiers générés - """ - # Créer le répertoire de sortie si nécessaire - os.makedirs(output_path, exist_ok=True) - - # Chemins des fichiers de sortie - json_path = os.path.join(output_path, "ticket_analysis.json") - md_path = os.path.join(output_path, "ticket_analysis.md") - - # Sauvegarder au format JSON - with open(json_path, "w", encoding="utf-8") as f: - json.dump({"entries": self.entries}, f, indent=2, ensure_ascii=False) - - # Générer le contenu Markdown - md_content = self._generer_markdown() - - # Normaliser les accents dans le contenu Markdown avant de l'écrire - md_content = normaliser_accents(md_content) - - # Sauvegarder au format Markdown - with open(md_path, "w", encoding="utf-8") as f: - f.write(md_content) - - return { - "json": json_path, - "markdown": md_path - } - - def _generer_markdown(self) -> str: - """ - Génère le contenu Markdown à partir des entrées du journal. - - Returns: - Contenu Markdown formaté - """ - contenu = ["# Analyse de ticket de support\n"] - - # Statistiques - stats = self._calculer_statistiques() - contenu.append("## Statistiques\n") - contenu.append(f"- Images analysées: {stats['images_total']} ({stats['images_pertinentes']} pertinentes)") - contenu.append(f"- Questions identifiées: {stats['questions']}") - contenu.append(f"- Réponses identifiées: {stats['reponses']}") - contenu.append("\n") - - # Paramètres LLM globaux - if self.llm_params: - contenu.append("## Paramètres LLM globaux\n") - for param, valeur in self.llm_params.items(): - contenu.append(f"- **{param}**: {valeur}") - contenu.append("\n") - - # Paramètres LLM utilisés - contenu.append("## Paramètres LLM par agent\n") - - # Filtre d'images - agent_filtre_params = self.agent_filtre.obtenir_parametres_llm() - contenu.append("### Agent de filtrage d'images\n") - contenu.append(f"- **Type de LLM**: {self.agent_filtre.llm.__class__.__name__}") - contenu.append(f"- **Modèle**: {agent_filtre_params.get('model', 'Non spécifié')}") - contenu.append(f"- **Température**: {agent_filtre_params.get('temperature', 'Non spécifiée')}") - contenu.append(f"- **Tokens max**: {agent_filtre_params.get('max_tokens', 'Non spécifié')}") - - # Analyse d'images - agent_analyse_params = self.agent_analyse.obtenir_parametres_llm() - contenu.append("\n### Agent d'analyse d'images\n") - contenu.append(f"- **Type de LLM**: {self.agent_analyse.llm.__class__.__name__}") - contenu.append(f"- **Modèle**: {agent_analyse_params.get('model', 'Non spécifié')}") - contenu.append(f"- **Température**: {agent_analyse_params.get('temperature', 'Non spécifiée')}") - contenu.append(f"- **Tokens max**: {agent_analyse_params.get('max_tokens', 'Non spécifié')}") - - # Questions-réponses - agent_qr_params = self.agent_qr.obtenir_parametres_llm() - contenu.append("\n### Agent d'extraction questions-réponses\n") - contenu.append(f"- **Type de LLM**: {self.agent_qr.llm.__class__.__name__}") - contenu.append(f"- **Modèle**: {agent_qr_params.get('model', 'Non spécifié')}") - contenu.append(f"- **Température**: {agent_qr_params.get('temperature', 'Non spécifiée')}") - contenu.append(f"- **Tokens max**: {agent_qr_params.get('max_tokens', 'Non spécifié')}") - contenu.append("\n") - - # Actions chronologiques - contenu.append("## Journal d'actions\n") - - for entry in self.entries: - timestamp = entry.get("timestamp", "") - action = entry.get("action", "") - agent = entry.get("agent", "") - model = entry.get("llm", {}).get("model", "") - - # En-tête de l'action - contenu.append(f"### {timestamp} - {agent} ({model})") - contenu.append(f"**Action**: {action}") - - # Détails spécifiques selon le type d'action - if action == "filter_image": - image_path = entry.get("image_path", "") - response = entry.get("response", {}) - pertinente = response.get("pertinente", False) - type_image = response.get("type_image", "inconnue") - description = response.get("description", "") - - contenu.append(f"**Image**: {os.path.basename(image_path)}") - contenu.append(f"**Résultat**: {'Pertinente' if pertinente else 'Non pertinente'}") - contenu.append(f"**Type**: {type_image}") - contenu.append(f"**Description**: {description}") - - # Paramètres LLM utilisés - params_llm = response.get("parametres_llm", {}) - if params_llm: - contenu.append("\n**Paramètres LLM utilisés:**") - temp = params_llm.get("parametres", {}).get("temperature", "N/A") - contenu.append(f"- Température: {temp}") - - elif action == "analyze_image": - image_path = entry.get("image_path", "") - response = entry.get("response", "") - - contenu.append(f"**Image analysée**: {os.path.basename(image_path)}") - contenu.append("\n**Analyse**:") - contenu.append(f"```\n{response}\n```") - - elif action == "extract_questions_reponses": - response = entry.get("response", {}) - tableau_md = response.get("tableau_md", "") - - contenu.append(f"**Questions**: {response.get('nb_questions', 0)}") - contenu.append(f"**Réponses**: {response.get('nb_reponses', 0)}") - contenu.append("\n") - contenu.append(tableau_md) - - # Paramètres LLM spécifiques à cette action - params_llm = entry.get("parametres_llm", {}) - if params_llm and action != "filter_image": # Pour éviter la duplication avec le filtre d'image - contenu.append("\n**Paramètres LLM utilisés:**") - for key, value in params_llm.items(): - if key != "system_prompt": # Éviter d'afficher le prompt système ici - contenu.append(f"- **{key}**: {value}") - - # Tokens utilisés - if "tokens" in entry: - tokens = entry["tokens"] - contenu.append("\n**Tokens utilisés**:") - contenu.append(f"- Prompt: {tokens.get('prompt_tokens', 0)}") - contenu.append(f"- Completion: {tokens.get('completion_tokens', 0)}") - contenu.append(f"- Total: {tokens.get('total_tokens', 0)}") - - contenu.append("\n---\n") - - return "\n".join(contenu) - - def _calculer_statistiques(self) -> Dict[str, int]: - """ - Calcule les statistiques à partir des entrées du journal. - - Returns: - Dictionnaire de statistiques - """ - stats = { - "images_total": 0, - "images_pertinentes": 0, - "questions": 0, - "reponses": 0 - } - - for entry in self.entries: - action = entry.get("action", "") - - if action == "filter_image": - stats["images_total"] += 1 - if entry.get("response", {}).get("pertinente", False): - stats["images_pertinentes"] += 1 - - elif action == "extract_questions_reponses": - stats["questions"] = entry.get("response", {}).get("nb_questions", 0) - stats["reponses"] = entry.get("response", {}).get("nb_reponses", 0) - - return stats \ No newline at end of file diff --git a/utils/ticket_manager.py b/utils/ticket_manager.py deleted file mode 100644 index aae8481..0000000 --- a/utils/ticket_manager.py +++ /dev/null @@ -1,408 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Module pour gérer l'extraction de tickets depuis Odoo. -Cette version est simplifiée et indépendante de odoo_toolkit. -""" - -import os -import json -import base64 -from typing import Dict, List, Any, Optional -import requests -from agents import AgentFiltreImages, AgentAnalyseImage, AgentQuestionReponse - -class TicketManager: - """ - Gestionnaire de tickets pour extraire des données depuis Odoo. - """ - - def __init__(self, url: str, db: str, username: str, api_key: str): - """ - Initialise le gestionnaire de tickets avec les paramètres de connexion. - - Args: - url: URL du serveur Odoo - db: Nom de la base de données - username: Nom d'utilisateur - api_key: Clé API ou mot de passe - """ - self.url = url - self.db = db - self.username = username - self.api_key = api_key - self.uid = None - self.session_id = None - self.model_name = "project.task" # Modèle par défaut pour les tickets - - def login(self) -> bool: - """ - Établit la connexion au serveur Odoo. - - Returns: - True si la connexion réussit, False sinon - """ - try: - # Point d'entrée pour le login - login_url = f"{self.url}/web/session/authenticate" - - # Données pour la requête de login - login_data = { - "jsonrpc": "2.0", - "params": { - "db": self.db, - "login": self.username, - "password": self.api_key - } - } - - # Effectuer la requête - response = requests.post(login_url, json=login_data) - response.raise_for_status() - - # Extraire les résultats - result = response.json() - if result.get("error"): - print(f"Erreur de connexion: {result['error']['message']}") - return False - - # Récupérer l'ID utilisateur et la session - self.uid = result.get("result", {}).get("uid") - self.session_id = response.cookies.get("session_id") - - if not self.uid: - print("Erreur: Impossible de récupérer l'ID utilisateur") - return False - - print(f"Connecté avec succès à {self.url} (User ID: {self.uid})") - return True - - except Exception as e: - print(f"Erreur de connexion: {str(e)}") - return False - - def _ensure_connection(self) -> bool: - """ - Vérifie que la connexion est établie, tente de se reconnecter si nécessaire. - - Returns: - True si la connexion est disponible, False sinon - """ - if not self.uid or not self.session_id: - return self.login() - return True - - def _rpc_call(self, endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]: - """ - Effectue un appel RPC vers le serveur Odoo. - - Args: - endpoint: Point d'entrée de l'API (/web/dataset/call_kw, etc.) - params: Paramètres de la requête - - Returns: - Résultat de la requête - """ - if not self._ensure_connection(): - return {"error": "Non connecté"} - - try: - # Préparer la requête - full_url = f"{self.url}{endpoint}" - headers = {"Content-Type": "application/json"} - - # Données de la requête - data = { - "jsonrpc": "2.0", - "method": "call", - "params": params - } - - # Effectuer la requête - response = requests.post( - full_url, - json=data, - headers=headers, - cookies={"session_id": self.session_id} if self.session_id else None - ) - response.raise_for_status() - - # Traiter la réponse - result = response.json() - if result.get("error"): - return {"error": result["error"]["message"]} - - return result.get("result", {}) - - except Exception as e: - return {"error": str(e)} - - def search_read(self, model: str, domain: List, fields: List[str], limit: int = 0) -> List[Dict[str, Any]]: - """ - Recherche et lit des enregistrements selon un domaine. - - Args: - model: Nom du modèle - domain: Domaine de recherche - fields: Champs à récupérer - limit: Nombre max de résultats (0 pour illimité) - - Returns: - Liste des enregistrements trouvés - """ - params = { - "model": model, - "method": "search_read", - "args": [domain, fields], - "kwargs": {"limit": limit} - } - - result = self._rpc_call("/web/dataset/call_kw", params) - if isinstance(result, dict) and "error" in result: - print(f"Erreur lors de la recherche: {result['error']}") - return [] - - return result if isinstance(result, list) else [] - - def read(self, model: str, ids: List[int], fields: List[str]) -> List[Dict[str, Any]]: - """ - Lit des enregistrements par leurs IDs. - - Args: - model: Nom du modèle - ids: Liste des IDs à lire - fields: Champs à récupérer - - Returns: - Liste des enregistrements lus - """ - params = { - "model": model, - "method": "read", - "args": [ids, fields], - "kwargs": {} - } - - result = self._rpc_call("/web/dataset/call_kw", params) - if isinstance(result, dict) and "error" in result: - print(f"Erreur lors de la lecture: {result['error']}") - return [] - - return result if isinstance(result, list) else [] - - def get_ticket_by_code(self, ticket_code: str) -> Dict[str, Any]: - """ - Récupère un ticket par son code. - - Args: - ticket_code: Code du ticket à récupérer - - Returns: - Données du ticket ou dictionnaire vide si non trouvé - """ - # Rechercher l'ID du ticket par son code - tickets = self.search_read( - model=self.model_name, - domain=[("code", "=", ticket_code)], - fields=["id"], - limit=1 - ) - - if not tickets: - print(f"Aucun ticket trouvé avec le code {ticket_code}") - return {} - - # Récupérer toutes les données du ticket - ticket_id = tickets[0]["id"] - return self.get_ticket_by_id(ticket_id) - - def get_ticket_by_id(self, ticket_id: int) -> Dict[str, Any]: - """ - Récupère un ticket par son ID. - - Args: - ticket_id: ID du ticket à récupérer - - Returns: - Données du ticket ou dictionnaire vide si non trouvé - """ - # Récupérer les champs disponibles pour le modèle - fields_info = self._get_model_fields(self.model_name) - - # Lire les données du ticket - tickets = self.read( - model=self.model_name, - ids=[ticket_id], - fields=fields_info - ) - - if not tickets: - print(f"Aucun ticket trouvé avec l'ID {ticket_id}") - return {} - - return tickets[0] - - def _get_model_fields(self, model_name: str) -> List[str]: - """ - Récupère la liste des champs disponibles pour un modèle. - - Args: - model_name: Nom du modèle - - Returns: - Liste des noms de champs - """ - params = { - "model": model_name, - "method": "fields_get", - "args": [], - "kwargs": {"attributes": ["name", "type"]} - } - - result = self._rpc_call("/web/dataset/call_kw", params) - if "error" in result: - print(f"Erreur lors de la récupération des champs: {result['error']}") - return [] - - # Filtrer les types de champs problématiques - invalid_types = ["many2many", "binary"] - valid_fields = [ - field for field, info in result.items() - if info.get("type") not in invalid_types - ] - - return valid_fields - - def get_ticket_messages(self, ticket_id: int) -> List[Dict[str, Any]]: - """ - Récupère les messages d'un ticket. - - Args: - ticket_id: ID du ticket - - Returns: - Liste des messages du ticket - """ - # D'abord récupérer les IDs des messages - ticket = self.read( - model=self.model_name, - ids=[ticket_id], - fields=["message_ids"] - ) - - if not ticket or "message_ids" not in ticket[0]: - print(f"Impossible de récupérer les messages pour le ticket {ticket_id}") - return [] - - message_ids = ticket[0]["message_ids"] - - # Récupérer les détails des messages - messages = self.read( - model="mail.message", - ids=message_ids, - fields=["id", "body", "date", "author_id", "email_from", "subject", "parent_id"] - ) - - return messages - - def get_ticket_attachments(self, ticket_id: int, download_path: Optional[str] = None) -> List[Dict[str, Any]]: - """ - Récupère les pièces jointes d'un ticket, avec option de téléchargement. - - Args: - ticket_id: ID du ticket - download_path: Chemin où télécharger les pièces jointes (optionnel) - - Returns: - Liste des informations sur les pièces jointes - """ - # Rechercher les pièces jointes liées au ticket - attachments = self.search_read( - model="ir.attachment", - domain=[("res_model", "=", self.model_name), ("res_id", "=", ticket_id)], - fields=["id", "name", "mimetype", "create_date", "datas"] - ) - - if not attachments: - print(f"Aucune pièce jointe trouvée pour le ticket {ticket_id}") - return [] - - if download_path: - # Créer le répertoire si nécessaire - os.makedirs(download_path, exist_ok=True) - - # Télécharger chaque pièce jointe - for attachment in attachments: - if "datas" in attachment and attachment["datas"]: - # Déchiffrer les données base64 - binary_data = base64.b64decode(attachment["datas"]) - - # Nettoyer le nom de fichier - safe_name = attachment["name"].replace("/", "_").replace("\\", "_") - file_path = os.path.join(download_path, f"{attachment['id']}_{safe_name}") - - # Sauvegarder le fichier - with open(file_path, "wb") as f: - f.write(binary_data) - - # Remplacer les données binaires par le chemin du fichier - attachment["file_path"] = file_path - del attachment["datas"] - - return attachments - - def extract_ticket_data(self, ticket_id: int, output_dir: str) -> Dict[str, Any]: - """ - Extrait toutes les données d'un ticket, y compris messages et pièces jointes. - - Args: - ticket_id: ID du ticket - output_dir: Répertoire de sortie - - Returns: - Dictionnaire avec toutes les données du ticket - """ - # Créer le répertoire de sortie - os.makedirs(output_dir, exist_ok=True) - - # Récupérer les données du ticket - ticket = self.get_ticket_by_id(ticket_id) - if not ticket: - return {"error": f"Ticket {ticket_id} non trouvé"} - - # Sauvegarder les données du ticket - ticket_path = os.path.join(output_dir, "ticket_info.json") - with open(ticket_path, "w", encoding="utf-8") as f: - json.dump(ticket, f, indent=2, ensure_ascii=False) - - # Récupérer et sauvegarder les messages - messages = self.get_ticket_messages(ticket_id) - messages_path = os.path.join(output_dir, "messages.json") - with open(messages_path, "w", encoding="utf-8") as f: - json.dump(messages, f, indent=2, ensure_ascii=False) - - # Récupérer et sauvegarder les pièces jointes - attachments_dir = os.path.join(output_dir, "attachments") - attachments = self.get_ticket_attachments(ticket_id, attachments_dir) - attachments_path = os.path.join(output_dir, "attachments_info.json") - with open(attachments_path, "w", encoding="utf-8") as f: - json.dump(attachments, f, indent=2, ensure_ascii=False) - - # Compiler toutes les informations - result = { - "ticket": ticket, - "messages": messages, - "attachments": [ - {k: v for k, v in a.items() if k != "datas"} - for a in attachments - ], - "files": { - "ticket_info": ticket_path, - "messages": messages_path, - "attachments_info": attachments_path, - "attachments_dir": attachments_dir - } - } - - return result \ No newline at end of file diff --git a/venv/bin/Activate.ps1 b/venv/bin/Activate.ps1 deleted file mode 100644 index b49d77b..0000000 --- a/venv/bin/Activate.ps1 +++ /dev/null @@ -1,247 +0,0 @@ -<# -.Synopsis -Activate a Python virtual environment for the current PowerShell session. - -.Description -Pushes the python executable for a virtual environment to the front of the -$Env:PATH environment variable and sets the prompt to signify that you are -in a Python virtual environment. Makes use of the command line switches as -well as the `pyvenv.cfg` file values present in the virtual environment. - -.Parameter VenvDir -Path to the directory that contains the virtual environment to activate. The -default value for this is the parent of the directory that the Activate.ps1 -script is located within. - -.Parameter Prompt -The prompt prefix to display when this virtual environment is activated. By -default, this prompt is the name of the virtual environment folder (VenvDir) -surrounded by parentheses and followed by a single space (ie. '(.venv) '). - -.Example -Activate.ps1 -Activates the Python virtual environment that contains the Activate.ps1 script. - -.Example -Activate.ps1 -Verbose -Activates the Python virtual environment that contains the Activate.ps1 script, -and shows extra information about the activation as it executes. - -.Example -Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv -Activates the Python virtual environment located in the specified location. - -.Example -Activate.ps1 -Prompt "MyPython" -Activates the Python virtual environment that contains the Activate.ps1 script, -and prefixes the current prompt with the specified string (surrounded in -parentheses) while the virtual environment is active. - -.Notes -On Windows, it may be required to enable this Activate.ps1 script by setting the -execution policy for the user. You can do this by issuing the following PowerShell -command: - -PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser - -For more information on Execution Policies: -https://go.microsoft.com/fwlink/?LinkID=135170 - -#> -Param( - [Parameter(Mandatory = $false)] - [String] - $VenvDir, - [Parameter(Mandatory = $false)] - [String] - $Prompt -) - -<# Function declarations --------------------------------------------------- #> - -<# -.Synopsis -Remove all shell session elements added by the Activate script, including the -addition of the virtual environment's Python executable from the beginning of -the PATH variable. - -.Parameter NonDestructive -If present, do not remove this function from the global namespace for the -session. - -#> -function global:deactivate ([switch]$NonDestructive) { - # Revert to original values - - # The prior prompt: - if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { - Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt - Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT - } - - # The prior PYTHONHOME: - if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { - Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME - Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME - } - - # The prior PATH: - if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { - Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH - Remove-Item -Path Env:_OLD_VIRTUAL_PATH - } - - # Just remove the VIRTUAL_ENV altogether: - if (Test-Path -Path Env:VIRTUAL_ENV) { - Remove-Item -Path env:VIRTUAL_ENV - } - - # Just remove VIRTUAL_ENV_PROMPT altogether. - if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { - Remove-Item -Path env:VIRTUAL_ENV_PROMPT - } - - # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: - if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { - Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force - } - - # Leave deactivate function in the global namespace if requested: - if (-not $NonDestructive) { - Remove-Item -Path function:deactivate - } -} - -<# -.Description -Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the -given folder, and returns them in a map. - -For each line in the pyvenv.cfg file, if that line can be parsed into exactly -two strings separated by `=` (with any amount of whitespace surrounding the =) -then it is considered a `key = value` line. The left hand string is the key, -the right hand is the value. - -If the value starts with a `'` or a `"` then the first and last character is -stripped from the value before being captured. - -.Parameter ConfigDir -Path to the directory that contains the `pyvenv.cfg` file. -#> -function Get-PyVenvConfig( - [String] - $ConfigDir -) { - Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" - - # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). - $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue - - # An empty map will be returned if no config file is found. - $pyvenvConfig = @{ } - - if ($pyvenvConfigPath) { - - Write-Verbose "File exists, parse `key = value` lines" - $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath - - $pyvenvConfigContent | ForEach-Object { - $keyval = $PSItem -split "\s*=\s*", 2 - if ($keyval[0] -and $keyval[1]) { - $val = $keyval[1] - - # Remove extraneous quotations around a string value. - if ("'""".Contains($val.Substring(0, 1))) { - $val = $val.Substring(1, $val.Length - 2) - } - - $pyvenvConfig[$keyval[0]] = $val - Write-Verbose "Adding Key: '$($keyval[0])'='$val'" - } - } - } - return $pyvenvConfig -} - - -<# Begin Activate script --------------------------------------------------- #> - -# Determine the containing directory of this script -$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition -$VenvExecDir = Get-Item -Path $VenvExecPath - -Write-Verbose "Activation script is located in path: '$VenvExecPath'" -Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" -Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" - -# Set values required in priority: CmdLine, ConfigFile, Default -# First, get the location of the virtual environment, it might not be -# VenvExecDir if specified on the command line. -if ($VenvDir) { - Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" -} -else { - Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." - $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") - Write-Verbose "VenvDir=$VenvDir" -} - -# Next, read the `pyvenv.cfg` file to determine any required value such -# as `prompt`. -$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir - -# Next, set the prompt from the command line, or the config file, or -# just use the name of the virtual environment folder. -if ($Prompt) { - Write-Verbose "Prompt specified as argument, using '$Prompt'" -} -else { - Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" - if ($pyvenvCfg -and $pyvenvCfg['prompt']) { - Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" - $Prompt = $pyvenvCfg['prompt']; - } - else { - Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" - Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" - $Prompt = Split-Path -Path $venvDir -Leaf - } -} - -Write-Verbose "Prompt = '$Prompt'" -Write-Verbose "VenvDir='$VenvDir'" - -# Deactivate any currently active virtual environment, but leave the -# deactivate function in place. -deactivate -nondestructive - -# Now set the environment variable VIRTUAL_ENV, used by many tools to determine -# that there is an activated venv. -$env:VIRTUAL_ENV = $VenvDir - -if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { - - Write-Verbose "Setting prompt to '$Prompt'" - - # Set the prompt to include the env name - # Make sure _OLD_VIRTUAL_PROMPT is global - function global:_OLD_VIRTUAL_PROMPT { "" } - Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT - New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt - - function global:prompt { - Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " - _OLD_VIRTUAL_PROMPT - } - $env:VIRTUAL_ENV_PROMPT = $Prompt -} - -# Clear PYTHONHOME -if (Test-Path -Path Env:PYTHONHOME) { - Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME - Remove-Item -Path Env:PYTHONHOME -} - -# Add the venv to the PATH -Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH -$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/venv/bin/activate b/venv/bin/activate deleted file mode 100644 index 62061a8..0000000 --- a/venv/bin/activate +++ /dev/null @@ -1,70 +0,0 @@ -# This file must be used with "source bin/activate" *from bash* -# You cannot run it directly - -deactivate () { - # reset old environment variables - if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then - PATH="${_OLD_VIRTUAL_PATH:-}" - export PATH - unset _OLD_VIRTUAL_PATH - fi - if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then - PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" - export PYTHONHOME - unset _OLD_VIRTUAL_PYTHONHOME - fi - - # Call hash to forget past commands. Without forgetting - # past commands the $PATH changes we made may not be respected - hash -r 2> /dev/null - - if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then - PS1="${_OLD_VIRTUAL_PS1:-}" - export PS1 - unset _OLD_VIRTUAL_PS1 - fi - - unset VIRTUAL_ENV - unset VIRTUAL_ENV_PROMPT - if [ ! "${1:-}" = "nondestructive" ] ; then - # Self destruct! - unset -f deactivate - fi -} - -# unset irrelevant variables -deactivate nondestructive - -# on Windows, a path can contain colons and backslashes and has to be converted: -if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then - # transform D:\path\to\venv to /d/path/to/venv on MSYS - # and to /cygdrive/d/path/to/venv on Cygwin - export VIRTUAL_ENV=$(cygpath /home/fgras-ca/llm-ticket3/venv) -else - # use the path as-is - export VIRTUAL_ENV=/home/fgras-ca/llm-ticket3/venv -fi - -_OLD_VIRTUAL_PATH="$PATH" -PATH="$VIRTUAL_ENV/"bin":$PATH" -export PATH - -# unset PYTHONHOME if set -# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) -# could use `if (set -u; : $PYTHONHOME) ;` in bash -if [ -n "${PYTHONHOME:-}" ] ; then - _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" - unset PYTHONHOME -fi - -if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then - _OLD_VIRTUAL_PS1="${PS1:-}" - PS1='(venv) '"${PS1:-}" - export PS1 - VIRTUAL_ENV_PROMPT='(venv) ' - export VIRTUAL_ENV_PROMPT -fi - -# Call hash to forget past commands. Without forgetting -# past commands the $PATH changes we made may not be respected -hash -r 2> /dev/null diff --git a/venv/bin/activate.csh b/venv/bin/activate.csh deleted file mode 100644 index 608952b..0000000 --- a/venv/bin/activate.csh +++ /dev/null @@ -1,27 +0,0 @@ -# This file must be used with "source bin/activate.csh" *from csh*. -# You cannot run it directly. - -# Created by Davide Di Blasi . -# Ported to Python 3.3 venv by Andrew Svetlov - -alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' - -# Unset irrelevant variables. -deactivate nondestructive - -setenv VIRTUAL_ENV /home/fgras-ca/llm-ticket3/venv - -set _OLD_VIRTUAL_PATH="$PATH" -setenv PATH "$VIRTUAL_ENV/"bin":$PATH" - - -set _OLD_VIRTUAL_PROMPT="$prompt" - -if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then - set prompt = '(venv) '"$prompt" - setenv VIRTUAL_ENV_PROMPT '(venv) ' -endif - -alias pydoc python -m pydoc - -rehash diff --git a/venv/bin/activate.fish b/venv/bin/activate.fish deleted file mode 100644 index 478275d..0000000 --- a/venv/bin/activate.fish +++ /dev/null @@ -1,69 +0,0 @@ -# This file must be used with "source /bin/activate.fish" *from fish* -# (https://fishshell.com/). You cannot run it directly. - -function deactivate -d "Exit virtual environment and return to normal shell environment" - # reset old environment variables - if test -n "$_OLD_VIRTUAL_PATH" - set -gx PATH $_OLD_VIRTUAL_PATH - set -e _OLD_VIRTUAL_PATH - end - if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME - set -e _OLD_VIRTUAL_PYTHONHOME - end - - if test -n "$_OLD_FISH_PROMPT_OVERRIDE" - set -e _OLD_FISH_PROMPT_OVERRIDE - # prevents error when using nested fish instances (Issue #93858) - if functions -q _old_fish_prompt - functions -e fish_prompt - functions -c _old_fish_prompt fish_prompt - functions -e _old_fish_prompt - end - end - - set -e VIRTUAL_ENV - set -e VIRTUAL_ENV_PROMPT - if test "$argv[1]" != "nondestructive" - # Self-destruct! - functions -e deactivate - end -end - -# Unset irrelevant variables. -deactivate nondestructive - -set -gx VIRTUAL_ENV /home/fgras-ca/llm-ticket3/venv - -set -gx _OLD_VIRTUAL_PATH $PATH -set -gx PATH "$VIRTUAL_ENV/"bin $PATH - -# Unset PYTHONHOME if set. -if set -q PYTHONHOME - set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME - set -e PYTHONHOME -end - -if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" - # fish uses a function instead of an env var to generate the prompt. - - # Save the current fish_prompt function as the function _old_fish_prompt. - functions -c fish_prompt _old_fish_prompt - - # With the original prompt function renamed, we can override with our own. - function fish_prompt - # Save the return status of the last command. - set -l old_status $status - - # Output the venv prompt; color taken from the blue of the Python logo. - printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal) - - # Restore the return status of the previous command. - echo "exit $old_status" | . - # Output the original/"old" prompt. - _old_fish_prompt - end - - set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" - set -gx VIRTUAL_ENV_PROMPT '(venv) ' -end diff --git a/venv/bin/httpx b/venv/bin/httpx deleted file mode 100755 index 5884ee2..0000000 --- a/venv/bin/httpx +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/fgras-ca/llm-ticket3/venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from httpx import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/venv/bin/normalizer b/venv/bin/normalizer deleted file mode 100755 index a55484d..0000000 --- a/venv/bin/normalizer +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/fgras-ca/llm-ticket3/venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from charset_normalizer import cli -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(cli.cli_detect()) diff --git a/venv/bin/pip b/venv/bin/pip deleted file mode 100755 index ae8d173..0000000 --- a/venv/bin/pip +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/fgras-ca/llm-ticket3/venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/venv/bin/pip3 b/venv/bin/pip3 deleted file mode 100755 index ae8d173..0000000 --- a/venv/bin/pip3 +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/fgras-ca/llm-ticket3/venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/venv/bin/pip3.12 b/venv/bin/pip3.12 deleted file mode 100755 index ae8d173..0000000 --- a/venv/bin/pip3.12 +++ /dev/null @@ -1,8 +0,0 @@ -#!/home/fgras-ca/llm-ticket3/venv/bin/python3 -# -*- coding: utf-8 -*- -import re -import sys -from pip._internal.cli.main import main -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - sys.exit(main()) diff --git a/venv/bin/python b/venv/bin/python deleted file mode 120000 index b8a0adb..0000000 --- a/venv/bin/python +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/venv/bin/python3 b/venv/bin/python3 deleted file mode 120000 index ae65fda..0000000 --- a/venv/bin/python3 +++ /dev/null @@ -1 +0,0 @@ -/usr/bin/python3 \ No newline at end of file diff --git a/venv/bin/python3.12 b/venv/bin/python3.12 deleted file mode 120000 index b8a0adb..0000000 --- a/venv/bin/python3.12 +++ /dev/null @@ -1 +0,0 @@ -python3 \ No newline at end of file diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/AUTHORS b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/AUTHORS deleted file mode 100644 index 84c8d7e..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -Original Author ---------------- -Sébastien Alix , diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/LICENSE b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/LICENSE deleted file mode 100644 index 65c5ca8..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/METADATA b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/METADATA deleted file mode 100644 index 4e5afba..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/METADATA +++ /dev/null @@ -1,163 +0,0 @@ -Metadata-Version: 2.1 -Name: OdooRPC -Version: 0.10.1 -Summary: OdooRPC is a Python package providing an easy way to pilot your Odoo servers through RPC. -Home-page: https://github.com/OCA/odoorpc -Author: Sebastien Alix -Author-email: seb@usr-src.org -License: LGPL v3 -Keywords: openerp odoo server rpc client xml-rpc xmlrpc jsonrpc json-rpc odoorpc oerplib communication lib library python service web webservice -Classifier: Intended Audience :: Developers -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Framework :: Odoo -Description-Content-Type: text/x-rst -License-File: LICENSE -License-File: AUTHORS - -======= -OdooRPC -======= - -.. image:: https://img.shields.io/pypi/v/OdooRPC.svg - :target: https://pypi.python.org/pypi/OdooRPC/ - :alt: Latest Version - -.. image:: https://travis-ci.org/OCA/odoorpc.svg?branch=master - :target: https://travis-ci.org/OCA/odoorpc - :alt: Build Status - -.. image:: https://img.shields.io/pypi/pyversions/OdooRPC.svg - :target: https://pypi.python.org/pypi/OdooRPC/ - :alt: Supported Python versions - -.. image:: https://img.shields.io/pypi/l/OdooRPC.svg - :target: https://pypi.python.org/pypi/OdooRPC/ - :alt: License - -**OdooRPC** is a Python package providing an easy way to -pilot your **Odoo** servers through `RPC`. - -Features supported: - - access to all data model methods (even ``browse``) with an API similar - to the server-side API, - - use named parameters with model methods, - - user context automatically sent providing support for - internationalization, - - browse records, - - execute workflows, - - manage databases, - - reports downloading, - - JSON-RPC protocol (SSL supported), - -How does it work? See below: - -.. code-block:: python - - import odoorpc - - # Prepare the connection to the server - odoo = odoorpc.ODOO('localhost', port=8069) - - # Check available databases - print(odoo.db.list()) - - # Login - odoo.login('db_name', 'user', 'passwd') - - # Current user - user = odoo.env.user - print(user.name) # name of the user connected - print(user.company_id.name) # the name of its company - - # Simple 'raw' query - user_data = odoo.execute('res.users', 'read', [user.id]) - print(user_data) - - # Use all methods of a model - if 'sale.order' in odoo.env: - Order = odoo.env['sale.order'] - order_ids = Order.search([]) - for order in Order.browse(order_ids): - print(order.name) - products = [line.product_id.name for line in order.order_line] - print(products) - - # Update data through a record - user.name = "Brian Jones" - -See the documentation for more details and features. - -Supported Odoo server versions -============================== - -`OdooRPC` is tested on all major releases of `Odoo` (starting from 8.0). - -Supported Python versions -========================= - -`OdooRPC` support Python 2.7, 3.7+. - -License -======= - -This software is made available under the `LGPL v3` license. - -Generate the documentation -========================== - -To generate the documentation, you have to install `Sphinx` documentation -generator:: - - pip install sphinx - -Then, you can use the ``build_doc`` option of the ``setup.py``:: - - python setup.py build_doc - -The generated documentation will be in the ``./doc/build/html`` directory. - -Changes in this version -======================= - -Consult the ``CHANGELOG`` file. - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. - -Credits -======= - -Contributors ------------- - -* Sébastien Alix - -Do not contact contributors directly about support or help with technical issues. - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This package is maintained by the OCA. - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/RECORD b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/RECORD deleted file mode 100644 index 46a22ea..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/RECORD +++ /dev/null @@ -1,34 +0,0 @@ -OdooRPC-0.10.1.dist-info/AUTHORS,sha256=Kjdl6zj2iQulcwF4iADsfzyuusIPWLKsRK9rM2Bh4TY,95 -OdooRPC-0.10.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -OdooRPC-0.10.1.dist-info/LICENSE,sha256=2n6rt7r999OuXp8iOqW9we7ORaxWncIbOwN1ILRGR2g,7651 -OdooRPC-0.10.1.dist-info/METADATA,sha256=UuFVcRgJiOT8MOZ9sREZ4ebCik2JUuM8yckCO1HP9so,4803 -OdooRPC-0.10.1.dist-info/RECORD,, -OdooRPC-0.10.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -OdooRPC-0.10.1.dist-info/WHEEL,sha256=m9WAupmBd2JGDsXWQGJgMGXIWbQY3F5c2xBJbBhq0nY,110 -OdooRPC-0.10.1.dist-info/top_level.txt,sha256=qdAy2XwLvCFM_VdG79vIgP3UV43zLZmvNqbdk4L1b6E,8 -odoorpc/__init__.py,sha256=Zk5CzvWtqvlGWNupLWb8OJQh69KdB9Mv8wfnmfTHVf8,2495 -odoorpc/__pycache__/__init__.cpython-312.pyc,, -odoorpc/__pycache__/db.cpython-312.pyc,, -odoorpc/__pycache__/env.cpython-312.pyc,, -odoorpc/__pycache__/error.cpython-312.pyc,, -odoorpc/__pycache__/fields.cpython-312.pyc,, -odoorpc/__pycache__/models.cpython-312.pyc,, -odoorpc/__pycache__/odoo.cpython-312.pyc,, -odoorpc/__pycache__/report.cpython-312.pyc,, -odoorpc/__pycache__/session.cpython-312.pyc,, -odoorpc/__pycache__/tools.cpython-312.pyc,, -odoorpc/db.py,sha256=cBZzZvnNc5lBC-InKFfRGTBH4psG5mZJ8UOl0GDXt9k,10178 -odoorpc/env.py,sha256=ncP9TnvCwtrD4aHcsv4rSeMXaXTUNajUgYAwQeAWXwQ,10119 -odoorpc/error.py,sha256=QkGjqv5Y0aHxvtuV7oRiFbNhAXz8AK1srmMRLIc0gfU,3284 -odoorpc/fields.py,sha256=Kf5af_m0TDz0k4lKFJLv75YUsu8ClwUOcsKWbTv8EHU,27004 -odoorpc/models.py,sha256=4gsHOcqp8vhN4N9U66B5cnleSbf5gO93gqn7jEZN7Lc,15034 -odoorpc/odoo.py,sha256=UQWQCJppn05XDOgpAdMRKXZEHH6Dv-LkFd6heJaAZ1w,22740 -odoorpc/report.py,sha256=zF_XJDNyDmRDiMVjjQZtgnTBg4iFZZakrw6nUvE8U5k,7396 -odoorpc/rpc/__init__.py,sha256=DFNJYDtwlCHo1d6xBAKV4bXziVoBJLJ8b-Bu85xIgvs,9465 -odoorpc/rpc/__pycache__/__init__.cpython-312.pyc,, -odoorpc/rpc/__pycache__/error.cpython-312.pyc,, -odoorpc/rpc/__pycache__/jsonrpclib.cpython-312.pyc,, -odoorpc/rpc/error.py,sha256=LOb2kvZmXNGy5ZWw6W6UKWvF75YqmcVvL017budrnts,349 -odoorpc/rpc/jsonrpclib.py,sha256=oY0eChMXUinC5YFjUcUO5ZWqt4ar9Dq2X0TJiFnpGb0,5342 -odoorpc/session.py,sha256=YXGVVTKCZMzGCwxoGGeo_XDO04JK2rojrji7o9TuWC8,5567 -odoorpc/tools.py,sha256=yYvMIreEDgZKSoQhZYD6W4xZpY2XppbTnttqHMR1i2w,3539 diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/REQUESTED b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/REQUESTED deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/WHEEL b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/WHEEL deleted file mode 100644 index 4657450..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/WHEEL +++ /dev/null @@ -1,6 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.41.1) -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any - diff --git a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/top_level.txt deleted file mode 100644 index 40035b8..0000000 --- a/venv/lib/python3.12/site-packages/OdooRPC-0.10.1.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -odoorpc diff --git a/venv/lib/python3.12/site-packages/README.rst b/venv/lib/python3.12/site-packages/README.rst deleted file mode 100644 index 741f2ad..0000000 --- a/venv/lib/python3.12/site-packages/README.rst +++ /dev/null @@ -1 +0,0 @@ -This is a dummy package designed to prevent namesquatting on PyPI. You should install `beautifulsoup4 `_ instead. diff --git a/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc b/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc deleted file mode 100644 index 0a61930..0000000 Binary files a/venv/lib/python3.12/site-packages/__pycache__/six.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc b/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc deleted file mode 100644 index 785e78d..0000000 Binary files a/venv/lib/python3.12/site-packages/__pycache__/typing_extensions.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA deleted file mode 100644 index 3ac05cf..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/METADATA +++ /dev/null @@ -1,295 +0,0 @@ -Metadata-Version: 2.3 -Name: annotated-types -Version: 0.7.0 -Summary: Reusable constraint types to use with typing.Annotated -Project-URL: Homepage, https://github.com/annotated-types/annotated-types -Project-URL: Source, https://github.com/annotated-types/annotated-types -Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases -Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds -License-File: LICENSE -Classifier: Development Status :: 4 - Beta -Classifier: Environment :: Console -Classifier: Environment :: MacOS X -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: Information Technology -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: POSIX :: Linux -Classifier: Operating System :: Unix -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Typing :: Typed -Requires-Python: >=3.8 -Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9' -Description-Content-Type: text/markdown - -# annotated-types - -[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) -[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types) -[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types) -[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE) - -[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of -adding context-specific metadata to existing types, and specifies that -`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special -logic for `x`. - -This package provides metadata objects which can be used to represent common -constraints such as upper and lower bounds on scalar values and collection sizes, -a `Predicate` marker for runtime checks, and -descriptions of how we intend these metadata to be interpreted. In some cases, -we also note alternative representations which do not require this package. - -## Install - -```bash -pip install annotated-types -``` - -## Examples - -```python -from typing import Annotated -from annotated_types import Gt, Len, Predicate - -class MyClass: - age: Annotated[int, Gt(18)] # Valid: 19, 20, ... - # Invalid: 17, 18, "19", 19.0, ... - factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ... - # Invalid: 4, 8, -2, 5.0, "prime", ... - - my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50] - # Invalid: (1, 2), ["abc"], [0] * 20 -``` - -## Documentation - -_While `annotated-types` avoids runtime checks for performance, users should not -construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`. -Downstream implementors may choose to raise an error, emit a warning, silently ignore -a metadata item, etc., if the metadata objects described below are used with an -incompatible type - or for any other reason!_ - -### Gt, Ge, Lt, Le - -Express inclusive and/or exclusive bounds on orderable values - which may be numbers, -dates, times, strings, sets, etc. Note that the boundary value need not be of the -same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]` -is fine, for example, and implies that the value is an integer x such that `x > 1.5`. - -We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)` -as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on -the `annotated-types` package. - -To be explicit, these types have the following meanings: - -* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum -* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum -* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum -* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum - -### Interval - -`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single -metadata object. `None` attributes should be ignored, and non-`None` attributes -treated as per the single bounds above. - -### MultipleOf - -`MultipleOf(multiple_of=x)` might be interpreted in two ways: - -1. Python semantics, implying `value % multiple_of == 0`, or -2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1), - where `int(value / multiple_of) == value / multiple_of`. - -We encourage users to be aware of these two common interpretations and their -distinct behaviours, especially since very large or non-integer numbers make -it easy to cause silent data corruption due to floating-point imprecision. - -We encourage libraries to carefully document which interpretation they implement. - -### MinLen, MaxLen, Len - -`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive. - -As well as `Len()` which can optionally include upper and lower bounds, we also -provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)` -and `Len(max_length=y)` respectively. - -`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`. - -Examples of usage: - -* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less -* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less -* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more -* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6 -* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8 - -#### Changed in v0.4.0 - -* `min_inclusive` has been renamed to `min_length`, no change in meaning -* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive** -* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic - meaning of the upper bound in slices vs. `Len` - -See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion. - -### Timezone - -`Timezone` can be used with a `datetime` or a `time` to express which timezones -are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime. -`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis)) -expresses that any timezone-aware datetime is allowed. You may also pass a specific -timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) -object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only -allow a specific timezone, though we note that this is often a symptom of fragile design. - -#### Changed in v0.x.x - -* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of - `timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries. - -### Unit - -`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of -a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]` -would be a float representing a velocity in meters per second. - -Please note that `annotated_types` itself makes no attempt to parse or validate -the unit string in any way. That is left entirely to downstream libraries, -such as [`pint`](https://pint.readthedocs.io) or -[`astropy.units`](https://docs.astropy.org/en/stable/units/). - -An example of how a library might use this metadata: - -```python -from annotated_types import Unit -from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args - -# given a type annotated with a unit: -Meters = Annotated[float, Unit("m")] - - -# you can cast the annotation to a specific unit type with any -# callable that accepts a string and returns the desired type -T = TypeVar("T") -def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None: - if get_origin(tp) is Annotated: - for arg in get_args(tp): - if isinstance(arg, Unit): - return unit_cls(arg.unit) - return None - - -# using `pint` -import pint -pint_unit = cast_unit(Meters, pint.Unit) - - -# using `astropy.units` -import astropy.units as u -astropy_unit = cast_unit(Meters, u.Unit) -``` - -### Predicate - -`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values. -Users should prefer the statically inspectable metadata above, but if you need -the full power and flexibility of arbitrary runtime predicates... here it is. - -For some common constraints, we provide generic types: - -* `IsLower = Annotated[T, Predicate(str.islower)]` -* `IsUpper = Annotated[T, Predicate(str.isupper)]` -* `IsDigit = Annotated[T, Predicate(str.isdigit)]` -* `IsFinite = Annotated[T, Predicate(math.isfinite)]` -* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]` -* `IsNan = Annotated[T, Predicate(math.isnan)]` -* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]` -* `IsInfinite = Annotated[T, Predicate(math.isinf)]` -* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]` - -so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer -(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`. - -Some libraries might have special logic to handle known or understandable predicates, -for example by checking for `str.isdigit` and using its presence to both call custom -logic to enforce digit-only strings, and customise some generated external schema. -Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in -favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`. - -To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner. - -We do not specify what behaviour should be expected for predicates that raise -an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently -skip invalid constraints, or statically raise an error; or it might try calling it -and then propagate or discard the resulting -`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object` -exception. We encourage libraries to document the behaviour they choose. - -### Doc - -`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used. - -It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools. - -It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`. - -This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md). - -### Integrating downstream types with `GroupedMetadata` - -Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata. -This can help reduce verbosity and cognitive overhead for users. -For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata: - -```python -from dataclasses import dataclass -from typing import Iterator -from annotated_types import GroupedMetadata, Ge - -@dataclass -class Field(GroupedMetadata): - ge: int | None = None - description: str | None = None - - def __iter__(self) -> Iterator[object]: - # Iterating over a GroupedMetadata object should yield annotated-types - # constraint metadata objects which describe it as fully as possible, - # and may include other unknown objects too. - if self.ge is not None: - yield Ge(self.ge) - if self.description is not None: - yield Description(self.description) -``` - -Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently. - -Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself. - -Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`. - -### Consuming metadata - -We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103). - -It is up to the implementer to determine how this metadata is used. -You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases. - -## Design & History - -This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic -and Hypothesis, with the goal of making it as easy as possible for end-users to -provide more informative annotations for use by runtime libraries. - -It is deliberately minimal, and following PEP-593 allows considerable downstream -discretion in what (if anything!) they choose to support. Nonetheless, we expect -that staying simple and covering _only_ the most common use-cases will give users -and maintainers the best experience we can. If you'd like more constraints for your -types - follow our lead, by defining them and documenting them downstream! diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/RECORD b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/RECORD deleted file mode 100644 index a66e278..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/RECORD +++ /dev/null @@ -1,10 +0,0 @@ -annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046 -annotated_types-0.7.0.dist-info/RECORD,, -annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87 -annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083 -annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819 -annotated_types/__pycache__/__init__.cpython-312.pyc,, -annotated_types/__pycache__/test_cases.cpython-312.pyc,, -annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421 diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL deleted file mode 100644 index 516596c..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.24.2 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE b/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE deleted file mode 100644 index d99323a..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2022 the 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/venv/lib/python3.12/site-packages/annotated_types/__init__.py b/venv/lib/python3.12/site-packages/annotated_types/__init__.py deleted file mode 100644 index 74e0dee..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types/__init__.py +++ /dev/null @@ -1,432 +0,0 @@ -import math -import sys -import types -from dataclasses import dataclass -from datetime import tzinfo -from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union - -if sys.version_info < (3, 8): - from typing_extensions import Protocol, runtime_checkable -else: - from typing import Protocol, runtime_checkable - -if sys.version_info < (3, 9): - from typing_extensions import Annotated, Literal -else: - from typing import Annotated, Literal - -if sys.version_info < (3, 10): - EllipsisType = type(Ellipsis) - KW_ONLY = {} - SLOTS = {} -else: - from types import EllipsisType - - KW_ONLY = {"kw_only": True} - SLOTS = {"slots": True} - - -__all__ = ( - 'BaseMetadata', - 'GroupedMetadata', - 'Gt', - 'Ge', - 'Lt', - 'Le', - 'Interval', - 'MultipleOf', - 'MinLen', - 'MaxLen', - 'Len', - 'Timezone', - 'Predicate', - 'LowerCase', - 'UpperCase', - 'IsDigits', - 'IsFinite', - 'IsNotFinite', - 'IsNan', - 'IsNotNan', - 'IsInfinite', - 'IsNotInfinite', - 'doc', - 'DocInfo', - '__version__', -) - -__version__ = '0.7.0' - - -T = TypeVar('T') - - -# arguments that start with __ are considered -# positional only -# see https://peps.python.org/pep-0484/#positional-only-arguments - - -class SupportsGt(Protocol): - def __gt__(self: T, __other: T) -> bool: - ... - - -class SupportsGe(Protocol): - def __ge__(self: T, __other: T) -> bool: - ... - - -class SupportsLt(Protocol): - def __lt__(self: T, __other: T) -> bool: - ... - - -class SupportsLe(Protocol): - def __le__(self: T, __other: T) -> bool: - ... - - -class SupportsMod(Protocol): - def __mod__(self: T, __other: T) -> T: - ... - - -class SupportsDiv(Protocol): - def __div__(self: T, __other: T) -> T: - ... - - -class BaseMetadata: - """Base class for all metadata. - - This exists mainly so that implementers - can do `isinstance(..., BaseMetadata)` while traversing field annotations. - """ - - __slots__ = () - - -@dataclass(frozen=True, **SLOTS) -class Gt(BaseMetadata): - """Gt(gt=x) implies that the value must be greater than x. - - It can be used with any type that supports the ``>`` operator, - including numbers, dates and times, strings, sets, and so on. - """ - - gt: SupportsGt - - -@dataclass(frozen=True, **SLOTS) -class Ge(BaseMetadata): - """Ge(ge=x) implies that the value must be greater than or equal to x. - - It can be used with any type that supports the ``>=`` operator, - including numbers, dates and times, strings, sets, and so on. - """ - - ge: SupportsGe - - -@dataclass(frozen=True, **SLOTS) -class Lt(BaseMetadata): - """Lt(lt=x) implies that the value must be less than x. - - It can be used with any type that supports the ``<`` operator, - including numbers, dates and times, strings, sets, and so on. - """ - - lt: SupportsLt - - -@dataclass(frozen=True, **SLOTS) -class Le(BaseMetadata): - """Le(le=x) implies that the value must be less than or equal to x. - - It can be used with any type that supports the ``<=`` operator, - including numbers, dates and times, strings, sets, and so on. - """ - - le: SupportsLe - - -@runtime_checkable -class GroupedMetadata(Protocol): - """A grouping of multiple objects, like typing.Unpack. - - `GroupedMetadata` on its own is not metadata and has no meaning. - All of the constraints and metadata should be fully expressable - in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. - - Concrete implementations should override `GroupedMetadata.__iter__()` - to add their own metadata. - For example: - - >>> @dataclass - >>> class Field(GroupedMetadata): - >>> gt: float | None = None - >>> description: str | None = None - ... - >>> def __iter__(self) -> Iterable[object]: - >>> if self.gt is not None: - >>> yield Gt(self.gt) - >>> if self.description is not None: - >>> yield Description(self.gt) - - Also see the implementation of `Interval` below for an example. - - Parsers should recognize this and unpack it so that it can be used - both with and without unpacking: - - - `Annotated[int, Field(...)]` (parser must unpack Field) - - `Annotated[int, *Field(...)]` (PEP-646) - """ # noqa: trailing-whitespace - - @property - def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: - return True - - def __iter__(self) -> Iterator[object]: - ... - - if not TYPE_CHECKING: - __slots__ = () # allow subclasses to use slots - - def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: - # Basic ABC like functionality without the complexity of an ABC - super().__init_subclass__(*args, **kwargs) - if cls.__iter__ is GroupedMetadata.__iter__: - raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") - - def __iter__(self) -> Iterator[object]: # noqa: F811 - raise NotImplementedError # more helpful than "None has no attribute..." type errors - - -@dataclass(frozen=True, **KW_ONLY, **SLOTS) -class Interval(GroupedMetadata): - """Interval can express inclusive or exclusive bounds with a single object. - - It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which - are interpreted the same way as the single-bound constraints. - """ - - gt: Union[SupportsGt, None] = None - ge: Union[SupportsGe, None] = None - lt: Union[SupportsLt, None] = None - le: Union[SupportsLe, None] = None - - def __iter__(self) -> Iterator[BaseMetadata]: - """Unpack an Interval into zero or more single-bounds.""" - if self.gt is not None: - yield Gt(self.gt) - if self.ge is not None: - yield Ge(self.ge) - if self.lt is not None: - yield Lt(self.lt) - if self.le is not None: - yield Le(self.le) - - -@dataclass(frozen=True, **SLOTS) -class MultipleOf(BaseMetadata): - """MultipleOf(multiple_of=x) might be interpreted in two ways: - - 1. Python semantics, implying ``value % multiple_of == 0``, or - 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` - - We encourage users to be aware of these two common interpretations, - and libraries to carefully document which they implement. - """ - - multiple_of: Union[SupportsDiv, SupportsMod] - - -@dataclass(frozen=True, **SLOTS) -class MinLen(BaseMetadata): - """ - MinLen() implies minimum inclusive length, - e.g. ``len(value) >= min_length``. - """ - - min_length: Annotated[int, Ge(0)] - - -@dataclass(frozen=True, **SLOTS) -class MaxLen(BaseMetadata): - """ - MaxLen() implies maximum inclusive length, - e.g. ``len(value) <= max_length``. - """ - - max_length: Annotated[int, Ge(0)] - - -@dataclass(frozen=True, **SLOTS) -class Len(GroupedMetadata): - """ - Len() implies that ``min_length <= len(value) <= max_length``. - - Upper bound may be omitted or ``None`` to indicate no upper length bound. - """ - - min_length: Annotated[int, Ge(0)] = 0 - max_length: Optional[Annotated[int, Ge(0)]] = None - - def __iter__(self) -> Iterator[BaseMetadata]: - """Unpack a Len into zone or more single-bounds.""" - if self.min_length > 0: - yield MinLen(self.min_length) - if self.max_length is not None: - yield MaxLen(self.max_length) - - -@dataclass(frozen=True, **SLOTS) -class Timezone(BaseMetadata): - """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). - - ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. - ``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be - tz-aware but any timezone is allowed. - - You may also pass a specific timezone string or tzinfo object such as - ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that - you only allow a specific timezone, though we note that this is often - a symptom of poor design. - """ - - tz: Union[str, tzinfo, EllipsisType, None] - - -@dataclass(frozen=True, **SLOTS) -class Unit(BaseMetadata): - """Indicates that the value is a physical quantity with the specified unit. - - It is intended for usage with numeric types, where the value represents the - magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` - or ``speed: Annotated[float, Unit('m/s')]``. - - Interpretation of the unit string is left to the discretion of the consumer. - It is suggested to follow conventions established by python libraries that work - with physical quantities, such as - - - ``pint`` : - - ``astropy.units``: - - For indicating a quantity with a certain dimensionality but without a specific unit - it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. - Note, however, ``annotated_types`` itself makes no use of the unit string. - """ - - unit: str - - -@dataclass(frozen=True, **SLOTS) -class Predicate(BaseMetadata): - """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. - - Users should prefer statically inspectable metadata, but if you need the full - power and flexibility of arbitrary runtime predicates... here it is. - - We provide a few predefined predicates for common string constraints: - ``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and - ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which - can be given special handling, and avoid indirection like ``lambda s: s.lower()``. - - Some libraries might have special logic to handle certain predicates, e.g. by - checking for `str.isdigit` and using its presence to both call custom logic to - enforce digit-only strings, and customise some generated external schema. - - We do not specify what behaviour should be expected for predicates that raise - an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently - skip invalid constraints, or statically raise an error; or it might try calling it - and then propagate or discard the resulting exception. - """ - - func: Callable[[Any], bool] - - def __repr__(self) -> str: - if getattr(self.func, "__name__", "") == "": - return f"{self.__class__.__name__}({self.func!r})" - if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( - namespace := getattr(self.func.__self__, "__name__", None) - ): - return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" - if isinstance(self.func, type(str.isascii)): # method descriptor - return f"{self.__class__.__name__}({self.func.__qualname__})" - return f"{self.__class__.__name__}({self.func.__name__})" - - -@dataclass -class Not: - func: Callable[[Any], bool] - - def __call__(self, __v: Any) -> bool: - return not self.func(__v) - - -_StrType = TypeVar("_StrType", bound=str) - -LowerCase = Annotated[_StrType, Predicate(str.islower)] -""" -Return True if the string is a lowercase string, False otherwise. - -A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. -""" # noqa: E501 -UpperCase = Annotated[_StrType, Predicate(str.isupper)] -""" -Return True if the string is an uppercase string, False otherwise. - -A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. -""" # noqa: E501 -IsDigit = Annotated[_StrType, Predicate(str.isdigit)] -IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 -""" -Return True if the string is a digit string, False otherwise. - -A string is a digit string if all characters in the string are digits and there is at least one character in the string. -""" # noqa: E501 -IsAscii = Annotated[_StrType, Predicate(str.isascii)] -""" -Return True if all characters in the string are ASCII, False otherwise. - -ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. -""" - -_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) -IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] -"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" -IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] -"""Return True if x is one of infinity or NaN, and False otherwise""" -IsNan = Annotated[_NumericType, Predicate(math.isnan)] -"""Return True if x is a NaN (not a number), and False otherwise.""" -IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] -"""Return True if x is anything but NaN (not a number), and False otherwise.""" -IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] -"""Return True if x is a positive or negative infinity, and False otherwise.""" -IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] -"""Return True if x is neither a positive or negative infinity, and False otherwise.""" - -try: - from typing_extensions import DocInfo, doc # type: ignore [attr-defined] -except ImportError: - - @dataclass(frozen=True, **SLOTS) - class DocInfo: # type: ignore [no-redef] - """ " - The return value of doc(), mainly to be used by tools that want to extract the - Annotated documentation at runtime. - """ - - documentation: str - """The documentation string passed to doc().""" - - def doc( - documentation: str, - ) -> DocInfo: - """ - Add documentation to a type annotation inside of Annotated. - - For example: - - >>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ... - """ - return DocInfo(documentation) diff --git a/venv/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 14fa328..0000000 Binary files a/venv/lib/python3.12/site-packages/annotated_types/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc b/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc deleted file mode 100644 index 973ebfd..0000000 Binary files a/venv/lib/python3.12/site-packages/annotated_types/__pycache__/test_cases.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/annotated_types/py.typed b/venv/lib/python3.12/site-packages/annotated_types/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/annotated_types/test_cases.py b/venv/lib/python3.12/site-packages/annotated_types/test_cases.py deleted file mode 100644 index d9164d6..0000000 --- a/venv/lib/python3.12/site-packages/annotated_types/test_cases.py +++ /dev/null @@ -1,151 +0,0 @@ -import math -import sys -from datetime import date, datetime, timedelta, timezone -from decimal import Decimal -from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple - -if sys.version_info < (3, 9): - from typing_extensions import Annotated -else: - from typing import Annotated - -import annotated_types as at - - -class Case(NamedTuple): - """ - A test case for `annotated_types`. - """ - - annotation: Any - valid_cases: Iterable[Any] - invalid_cases: Iterable[Any] - - -def cases() -> Iterable[Case]: - # Gt, Ge, Lt, Le - yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1)) - yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1)) - yield Case( - Annotated[datetime, at.Gt(datetime(2000, 1, 1))], - [datetime(2000, 1, 2), datetime(2000, 1, 3)], - [datetime(2000, 1, 1), datetime(1999, 12, 31)], - ) - yield Case( - Annotated[datetime, at.Gt(date(2000, 1, 1))], - [date(2000, 1, 2), date(2000, 1, 3)], - [date(2000, 1, 1), date(1999, 12, 31)], - ) - yield Case( - Annotated[datetime, at.Gt(Decimal('1.123'))], - [Decimal('1.1231'), Decimal('123')], - [Decimal('1.123'), Decimal('0')], - ) - - yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1)) - yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1)) - yield Case( - Annotated[datetime, at.Ge(datetime(2000, 1, 1))], - [datetime(2000, 1, 2), datetime(2000, 1, 3)], - [datetime(1998, 1, 1), datetime(1999, 12, 31)], - ) - - yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4)) - yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9)) - yield Case( - Annotated[datetime, at.Lt(datetime(2000, 1, 1))], - [datetime(1999, 12, 31), datetime(1999, 12, 31)], - [datetime(2000, 1, 2), datetime(2000, 1, 3)], - ) - - yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000)) - yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9)) - yield Case( - Annotated[datetime, at.Le(datetime(2000, 1, 1))], - [datetime(2000, 1, 1), datetime(1999, 12, 31)], - [datetime(2000, 1, 2), datetime(2000, 1, 3)], - ) - - # Interval - yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1)) - yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1)) - yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1)) - yield Case( - Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))], - [datetime(2000, 1, 2), datetime(2000, 1, 3)], - [datetime(2000, 1, 1), datetime(2000, 1, 4)], - ) - - yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4)) - yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1)) - - # lengths - - yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) - yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) - yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) - yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) - - yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10)) - yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10)) - yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) - yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) - - yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10)) - yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234')) - - yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}]) - yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4})) - yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4))) - - # Timezone - - yield Case( - Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)] - ) - yield Case( - Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)] - ) - yield Case( - Annotated[datetime, at.Timezone(timezone.utc)], - [datetime(2000, 1, 1, tzinfo=timezone.utc)], - [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], - ) - yield Case( - Annotated[datetime, at.Timezone('Europe/London')], - [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))], - [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], - ) - - # Quantity - - yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m')) - - # predicate types - - yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom']) - yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC']) - yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2']) - yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀']) - - yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5]) - - yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf]) - yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23]) - yield Case(at.IsNan[float], [math.nan], [1.23, math.inf]) - yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan]) - yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23]) - yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf]) - - # check stacked predicates - yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan]) - - # doc - yield Case(Annotated[int, at.doc("A number")], [1, 2], []) - - # custom GroupedMetadata - class MyCustomGroupedMetadata(at.GroupedMetadata): - def __iter__(self) -> Iterator[at.Predicate]: - yield at.Predicate(lambda x: float(x).is_integer()) - - yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5]) diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/LICENSE b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/LICENSE deleted file mode 100644 index 104eebf..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018 Alex Grönholm - -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/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/METADATA b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/METADATA deleted file mode 100644 index 9d87e1d..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/METADATA +++ /dev/null @@ -1,105 +0,0 @@ -Metadata-Version: 2.2 -Name: anyio -Version: 4.9.0 -Summary: High level compatibility layer for multiple asynchronous event loop implementations -Author-email: Alex Grönholm -License: MIT -Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/ -Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html -Project-URL: Source code, https://github.com/agronholm/anyio -Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Framework :: AnyIO -Classifier: Typing :: Typed -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Requires-Python: >=3.9 -Description-Content-Type: text/x-rst -License-File: LICENSE -Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11" -Requires-Dist: idna>=2.8 -Requires-Dist: sniffio>=1.1 -Requires-Dist: typing_extensions>=4.5; python_version < "3.13" -Provides-Extra: trio -Requires-Dist: trio>=0.26.1; extra == "trio" -Provides-Extra: test -Requires-Dist: anyio[trio]; extra == "test" -Requires-Dist: blockbuster>=1.5.23; extra == "test" -Requires-Dist: coverage[toml]>=7; extra == "test" -Requires-Dist: exceptiongroup>=1.2.0; extra == "test" -Requires-Dist: hypothesis>=4.0; extra == "test" -Requires-Dist: psutil>=5.9; extra == "test" -Requires-Dist: pytest>=7.0; extra == "test" -Requires-Dist: trustme; extra == "test" -Requires-Dist: truststore>=0.9.1; python_version >= "3.10" and extra == "test" -Requires-Dist: uvloop>=0.21; (platform_python_implementation == "CPython" and platform_system != "Windows" and python_version < "3.14") and extra == "test" -Provides-Extra: doc -Requires-Dist: packaging; extra == "doc" -Requires-Dist: Sphinx~=8.2; extra == "doc" -Requires-Dist: sphinx_rtd_theme; extra == "doc" -Requires-Dist: sphinx-autodoc-typehints>=1.2.0; extra == "doc" - -.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg - :target: https://github.com/agronholm/anyio/actions/workflows/test.yml - :alt: Build Status -.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master - :target: https://coveralls.io/github/agronholm/anyio?branch=master - :alt: Code Coverage -.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest - :target: https://anyio.readthedocs.io/en/latest/?badge=latest - :alt: Documentation -.. image:: https://badges.gitter.im/gitterHQ/gitter.svg - :target: https://gitter.im/python-trio/AnyIO - :alt: Gitter chat - -AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or -trio_. It implements trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony -with the native SC of trio itself. - -Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or -trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full -refactoring necessary. It will blend in with the native libraries of your chosen backend. - -Documentation -------------- - -View full documentation at: https://anyio.readthedocs.io/ - -Features --------- - -AnyIO offers the following functionality: - -* Task groups (nurseries_ in trio terminology) -* High-level networking (TCP, UDP and UNIX sockets) - - * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python - 3.8) - * async/await style UDP sockets (unlike asyncio where you still have to use Transports and - Protocols) - -* A versatile API for byte streams and object streams -* Inter-task synchronization and communication (locks, conditions, events, semaphores, object - streams) -* Worker threads -* Subprocesses -* Asynchronous file I/O (using worker threads) -* Signal handling - -AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures. -It even works with the popular Hypothesis_ library. - -.. _asyncio: https://docs.python.org/3/library/asyncio.html -.. _trio: https://github.com/python-trio/trio -.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency -.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning -.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs -.. _pytest: https://docs.pytest.org/en/latest/ -.. _Hypothesis: https://hypothesis.works/ diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/RECORD b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/RECORD deleted file mode 100644 index 40eedc8..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/RECORD +++ /dev/null @@ -1,88 +0,0 @@ -anyio-4.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -anyio-4.9.0.dist-info/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081 -anyio-4.9.0.dist-info/METADATA,sha256=vvkWPXXTbrpTCFK7zdcYwQcSQhx6Q4qITM9t_PEQCrY,4682 -anyio-4.9.0.dist-info/RECORD,, -anyio-4.9.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91 -anyio-4.9.0.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39 -anyio-4.9.0.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6 -anyio/__init__.py,sha256=t8bZuNXa5ncwXBaNKbv48BDgZt48RT_zCEtrnPmjNU8,4993 -anyio/__pycache__/__init__.cpython-312.pyc,, -anyio/__pycache__/from_thread.cpython-312.pyc,, -anyio/__pycache__/lowlevel.cpython-312.pyc,, -anyio/__pycache__/pytest_plugin.cpython-312.pyc,, -anyio/__pycache__/to_interpreter.cpython-312.pyc,, -anyio/__pycache__/to_process.cpython-312.pyc,, -anyio/__pycache__/to_thread.cpython-312.pyc,, -anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -anyio/_backends/__pycache__/__init__.cpython-312.pyc,, -anyio/_backends/__pycache__/_asyncio.cpython-312.pyc,, -anyio/_backends/__pycache__/_trio.cpython-312.pyc,, -anyio/_backends/_asyncio.py,sha256=AT1oaTfCE-9YFxooMlvld2yDqY5U2A-ANMcBDh9eRfI,93455 -anyio/_backends/_trio.py,sha256=HVfDqRGQ7Xj3JfTcYdgzmC7pZEplqU4NOO5kxNNSZnk,40429 -anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -anyio/_core/__pycache__/__init__.cpython-312.pyc,, -anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc,, -anyio/_core/__pycache__/_eventloop.cpython-312.pyc,, -anyio/_core/__pycache__/_exceptions.cpython-312.pyc,, -anyio/_core/__pycache__/_fileio.cpython-312.pyc,, -anyio/_core/__pycache__/_resources.cpython-312.pyc,, -anyio/_core/__pycache__/_signals.cpython-312.pyc,, -anyio/_core/__pycache__/_sockets.cpython-312.pyc,, -anyio/_core/__pycache__/_streams.cpython-312.pyc,, -anyio/_core/__pycache__/_subprocesses.cpython-312.pyc,, -anyio/_core/__pycache__/_synchronization.cpython-312.pyc,, -anyio/_core/__pycache__/_tasks.cpython-312.pyc,, -anyio/_core/__pycache__/_tempfile.cpython-312.pyc,, -anyio/_core/__pycache__/_testing.cpython-312.pyc,, -anyio/_core/__pycache__/_typedattr.cpython-312.pyc,, -anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626 -anyio/_core/_eventloop.py,sha256=t_tAwBFPjF8jrZGjlJ6bbYy6KA3bjsbZxV9mvh9t1i0,4695 -anyio/_core/_exceptions.py,sha256=RlPRlwastdmfDPoskdXNO6SI8_l3fclA2wtW6cokU9I,3503 -anyio/_core/_fileio.py,sha256=qFZhkLIz0cGXluvih_vcPUTucgq8UFVgsTCtYbijZIg,23340 -anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435 -anyio/_core/_signals.py,sha256=vulT1M1xdLYtAR-eY5TamIgaf1WTlOwOrMGwswlTTr8,905 -anyio/_core/_sockets.py,sha256=5Okc_UThGDEN9KCnsIhqWPRHBNuSy6b4NmG1i51TVF4,27150 -anyio/_core/_streams.py,sha256=OnaKgoDD-FcMSwLvkoAUGP51sG2ZdRvMpxt9q2w1gYA,1804 -anyio/_core/_subprocesses.py,sha256=EXm5igL7dj55iYkPlbYVAqtbqxJxjU-6OndSTIx9SRg,8047 -anyio/_core/_synchronization.py,sha256=DwUh8Tl6cG_UMVC_GyzPoC_U9BpfDfjMl9SINSxcZN4,20320 -anyio/_core/_tasks.py,sha256=f3CuWwo06cCZ6jaOv-JHFKWkgpgf2cvaF25Oh4augMA,4757 -anyio/_core/_tempfile.py,sha256=s-_ucacXbxBH5Bo5eo65lN0lPwZQd5B8yNN_9nARpCM,19696 -anyio/_core/_testing.py,sha256=YUGwA5cgFFbUTv4WFd7cv_BSVr4ryTtPp8owQA3JdWE,2118 -anyio/_core/_typedattr.py,sha256=P4ozZikn3-DbpoYcvyghS_FOYAgbmUxeoU8-L_07pZM,2508 -anyio/abc/__init__.py,sha256=c2OQbTCS_fQowviMXanLPh8m29ccwkXmpDr7uyNZYOo,2652 -anyio/abc/__pycache__/__init__.cpython-312.pyc,, -anyio/abc/__pycache__/_eventloop.cpython-312.pyc,, -anyio/abc/__pycache__/_resources.cpython-312.pyc,, -anyio/abc/__pycache__/_sockets.cpython-312.pyc,, -anyio/abc/__pycache__/_streams.cpython-312.pyc,, -anyio/abc/__pycache__/_subprocesses.cpython-312.pyc,, -anyio/abc/__pycache__/_tasks.cpython-312.pyc,, -anyio/abc/__pycache__/_testing.cpython-312.pyc,, -anyio/abc/_eventloop.py,sha256=UmL8DZCvQTgxzmyBZcGm9kWj9VQY8BMWueLh5S8yWN4,9682 -anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783 -anyio/abc/_sockets.py,sha256=KhWtJxan8jpBXKwPaFeQzI4iRXdFaOIn0HXtDZnaO7U,6262 -anyio/abc/_streams.py,sha256=He_JpkAW2g5veOzcUq0XsRC2nId_i35L-d8cs7Uj1ZQ,6598 -anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067 -anyio/abc/_tasks.py,sha256=yJWbMwowvqjlAX4oJ3l9Is1w-zwynr2lX1Z02AWJqsY,3080 -anyio/abc/_testing.py,sha256=tBJUzkSfOXJw23fe8qSJ03kJlShOYjjaEyFB6k6MYT8,1821 -anyio/from_thread.py,sha256=MbXHZpgM9wgsRkbGhMNMomEGYj7Y_QYq6a5BZ3c5Ev8,17478 -anyio/lowlevel.py,sha256=nkgmW--SdxGVp0cmLUYazjkigveRm5HY7-gW8Bpp9oY,4169 -anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -anyio/pytest_plugin.py,sha256=qXNwk9Pa7hPQKWocgLl9qijqKGMkGzdH2wJa-jPkGUM,9375 -anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -anyio/streams/__pycache__/__init__.cpython-312.pyc,, -anyio/streams/__pycache__/buffered.cpython-312.pyc,, -anyio/streams/__pycache__/file.cpython-312.pyc,, -anyio/streams/__pycache__/memory.cpython-312.pyc,, -anyio/streams/__pycache__/stapled.cpython-312.pyc,, -anyio/streams/__pycache__/text.cpython-312.pyc,, -anyio/streams/__pycache__/tls.cpython-312.pyc,, -anyio/streams/buffered.py,sha256=UCldKC168YuLvT7n3HtNPnQ2iWAMSTYQWbZvzLwMwkM,4500 -anyio/streams/file.py,sha256=6uoTNb5KbMoj-6gS3_xrrL8uZN8Q4iIvOS1WtGyFfKw,4383 -anyio/streams/memory.py,sha256=o1OVVx0OooteTTe2GytJreum93Ucuw5s4cAsr3X0-Ag,10560 -anyio/streams/stapled.py,sha256=U09pCrmOw9kkNhe6tKopsm1QIMT1lFTFvtb-A7SIe4k,4302 -anyio/streams/text.py,sha256=6x8w8xlfCZKTUWQoJiMPoMhSSJFUBRKgoBNSBtbd9yg,5094 -anyio/streams/tls.py,sha256=HxzpVmUgo8SUSIBass_lvef1pAI1uRSrnysM3iEGzl4,13199 -anyio/to_interpreter.py,sha256=UhuNCIucCRN7ZtyJg35Mlamzs1JpgDvK4xnL4TDWrAo,6527 -anyio/to_process.py,sha256=ZvruelRM-HNmqDaql4sdNODg2QD_uSlwSCxnV4OhsfQ,9595 -anyio/to_thread.py,sha256=WM2JQ2MbVsd5D5CM08bQiTwzZIvpsGjfH1Fy247KoDQ,2396 diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/WHEEL b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/WHEEL deleted file mode 100644 index 9c3ae63..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (76.0.0) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/entry_points.txt b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/entry_points.txt deleted file mode 100644 index 44dd9bd..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[pytest11] -anyio = anyio.pytest_plugin diff --git a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/top_level.txt deleted file mode 100644 index c77c069..0000000 --- a/venv/lib/python3.12/site-packages/anyio-4.9.0.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -anyio diff --git a/venv/lib/python3.12/site-packages/anyio/__init__.py b/venv/lib/python3.12/site-packages/anyio/__init__.py deleted file mode 100644 index 578cda6..0000000 --- a/venv/lib/python3.12/site-packages/anyio/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -from ._core._eventloop import current_time as current_time -from ._core._eventloop import get_all_backends as get_all_backends -from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class -from ._core._eventloop import run as run -from ._core._eventloop import sleep as sleep -from ._core._eventloop import sleep_forever as sleep_forever -from ._core._eventloop import sleep_until as sleep_until -from ._core._exceptions import BrokenResourceError as BrokenResourceError -from ._core._exceptions import BrokenWorkerIntepreter as BrokenWorkerIntepreter -from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess -from ._core._exceptions import BusyResourceError as BusyResourceError -from ._core._exceptions import ClosedResourceError as ClosedResourceError -from ._core._exceptions import DelimiterNotFound as DelimiterNotFound -from ._core._exceptions import EndOfStream as EndOfStream -from ._core._exceptions import IncompleteRead as IncompleteRead -from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError -from ._core._exceptions import WouldBlock as WouldBlock -from ._core._fileio import AsyncFile as AsyncFile -from ._core._fileio import Path as Path -from ._core._fileio import open_file as open_file -from ._core._fileio import wrap_file as wrap_file -from ._core._resources import aclose_forcefully as aclose_forcefully -from ._core._signals import open_signal_receiver as open_signal_receiver -from ._core._sockets import connect_tcp as connect_tcp -from ._core._sockets import connect_unix as connect_unix -from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket -from ._core._sockets import ( - create_connected_unix_datagram_socket as create_connected_unix_datagram_socket, -) -from ._core._sockets import create_tcp_listener as create_tcp_listener -from ._core._sockets import create_udp_socket as create_udp_socket -from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket -from ._core._sockets import create_unix_listener as create_unix_listener -from ._core._sockets import getaddrinfo as getaddrinfo -from ._core._sockets import getnameinfo as getnameinfo -from ._core._sockets import wait_readable as wait_readable -from ._core._sockets import wait_socket_readable as wait_socket_readable -from ._core._sockets import wait_socket_writable as wait_socket_writable -from ._core._sockets import wait_writable as wait_writable -from ._core._streams import create_memory_object_stream as create_memory_object_stream -from ._core._subprocesses import open_process as open_process -from ._core._subprocesses import run_process as run_process -from ._core._synchronization import CapacityLimiter as CapacityLimiter -from ._core._synchronization import ( - CapacityLimiterStatistics as CapacityLimiterStatistics, -) -from ._core._synchronization import Condition as Condition -from ._core._synchronization import ConditionStatistics as ConditionStatistics -from ._core._synchronization import Event as Event -from ._core._synchronization import EventStatistics as EventStatistics -from ._core._synchronization import Lock as Lock -from ._core._synchronization import LockStatistics as LockStatistics -from ._core._synchronization import ResourceGuard as ResourceGuard -from ._core._synchronization import Semaphore as Semaphore -from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics -from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED -from ._core._tasks import CancelScope as CancelScope -from ._core._tasks import create_task_group as create_task_group -from ._core._tasks import current_effective_deadline as current_effective_deadline -from ._core._tasks import fail_after as fail_after -from ._core._tasks import move_on_after as move_on_after -from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile -from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile -from ._core._tempfile import TemporaryDirectory as TemporaryDirectory -from ._core._tempfile import TemporaryFile as TemporaryFile -from ._core._tempfile import gettempdir as gettempdir -from ._core._tempfile import gettempdirb as gettempdirb -from ._core._tempfile import mkdtemp as mkdtemp -from ._core._tempfile import mkstemp as mkstemp -from ._core._testing import TaskInfo as TaskInfo -from ._core._testing import get_current_task as get_current_task -from ._core._testing import get_running_tasks as get_running_tasks -from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked -from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider -from ._core._typedattr import TypedAttributeSet as TypedAttributeSet -from ._core._typedattr import typed_attribute as typed_attribute - -# Re-export imports so they look like they live directly in this package -for __value in list(locals().values()): - if getattr(__value, "__module__", "").startswith("anyio."): - __value.__module__ = __name__ - -del __value diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index babd8f8..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc deleted file mode 100644 index 1deb21e..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/from_thread.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc deleted file mode 100644 index 8f3229c..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/lowlevel.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc deleted file mode 100644 index ada0a61..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/pytest_plugin.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc deleted file mode 100644 index 163fc0a..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_interpreter.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc deleted file mode 100644 index 7185618..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_process.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc deleted file mode 100644 index 7f1cbf5..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/__pycache__/to_thread.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__init__.py b/venv/lib/python3.12/site-packages/anyio/_backends/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 004a96f..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc deleted file mode 100644 index 4937a06..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc deleted file mode 100644 index 1fc8d4d..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_backends/__pycache__/_trio.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py b/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py deleted file mode 100644 index ed91f40..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py +++ /dev/null @@ -1,2816 +0,0 @@ -from __future__ import annotations - -import array -import asyncio -import concurrent.futures -import contextvars -import math -import os -import socket -import sys -import threading -import weakref -from asyncio import ( - AbstractEventLoop, - CancelledError, - all_tasks, - create_task, - current_task, - get_running_loop, - sleep, -) -from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined] -from collections import OrderedDict, deque -from collections.abc import ( - AsyncGenerator, - AsyncIterator, - Awaitable, - Callable, - Collection, - Coroutine, - Iterable, - Sequence, -) -from concurrent.futures import Future -from contextlib import AbstractContextManager, suppress -from contextvars import Context, copy_context -from dataclasses import dataclass -from functools import partial, wraps -from inspect import ( - CORO_RUNNING, - CORO_SUSPENDED, - getcoroutinestate, - iscoroutine, -) -from io import IOBase -from os import PathLike -from queue import Queue -from signal import Signals -from socket import AddressFamily, SocketKind -from threading import Thread -from types import CodeType, TracebackType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Optional, - TypeVar, - cast, -) -from weakref import WeakKeyDictionary - -import sniffio - -from .. import ( - CapacityLimiterStatistics, - EventStatistics, - LockStatistics, - TaskInfo, - abc, -) -from .._core._eventloop import claim_worker_thread, threadlocals -from .._core._exceptions import ( - BrokenResourceError, - BusyResourceError, - ClosedResourceError, - EndOfStream, - WouldBlock, - iterate_exceptions, -) -from .._core._sockets import convert_ipv6_sockaddr -from .._core._streams import create_memory_object_stream -from .._core._synchronization import ( - CapacityLimiter as BaseCapacityLimiter, -) -from .._core._synchronization import Event as BaseEvent -from .._core._synchronization import Lock as BaseLock -from .._core._synchronization import ( - ResourceGuard, - SemaphoreStatistics, -) -from .._core._synchronization import Semaphore as BaseSemaphore -from .._core._tasks import CancelScope as BaseCancelScope -from ..abc import ( - AsyncBackend, - IPSockAddrType, - SocketListener, - UDPPacketType, - UNIXDatagramPacketType, -) -from ..abc._eventloop import StrOrBytesPath -from ..lowlevel import RunVar -from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream - -if TYPE_CHECKING: - from _typeshed import FileDescriptorLike -else: - FileDescriptorLike = object - -if sys.version_info >= (3, 10): - from typing import ParamSpec -else: - from typing_extensions import ParamSpec - -if sys.version_info >= (3, 11): - from asyncio import Runner - from typing import TypeVarTuple, Unpack -else: - import contextvars - import enum - import signal - from asyncio import coroutines, events, exceptions, tasks - - from exceptiongroup import BaseExceptionGroup - from typing_extensions import TypeVarTuple, Unpack - - class _State(enum.Enum): - CREATED = "created" - INITIALIZED = "initialized" - CLOSED = "closed" - - class Runner: - # Copied from CPython 3.11 - def __init__( - self, - *, - debug: bool | None = None, - loop_factory: Callable[[], AbstractEventLoop] | None = None, - ): - self._state = _State.CREATED - self._debug = debug - self._loop_factory = loop_factory - self._loop: AbstractEventLoop | None = None - self._context = None - self._interrupt_count = 0 - self._set_event_loop = False - - def __enter__(self) -> Runner: - self._lazy_init() - return self - - def __exit__( - self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, - ) -> None: - self.close() - - def close(self) -> None: - """Shutdown and close event loop.""" - if self._state is not _State.INITIALIZED: - return - try: - loop = self._loop - _cancel_all_tasks(loop) - loop.run_until_complete(loop.shutdown_asyncgens()) - if hasattr(loop, "shutdown_default_executor"): - loop.run_until_complete(loop.shutdown_default_executor()) - else: - loop.run_until_complete(_shutdown_default_executor(loop)) - finally: - if self._set_event_loop: - events.set_event_loop(None) - loop.close() - self._loop = None - self._state = _State.CLOSED - - def get_loop(self) -> AbstractEventLoop: - """Return embedded event loop.""" - self._lazy_init() - return self._loop - - def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval: - """Run a coroutine inside the embedded event loop.""" - if not coroutines.iscoroutine(coro): - raise ValueError(f"a coroutine was expected, got {coro!r}") - - if events._get_running_loop() is not None: - # fail fast with short traceback - raise RuntimeError( - "Runner.run() cannot be called from a running event loop" - ) - - self._lazy_init() - - if context is None: - context = self._context - task = context.run(self._loop.create_task, coro) - - if ( - threading.current_thread() is threading.main_thread() - and signal.getsignal(signal.SIGINT) is signal.default_int_handler - ): - sigint_handler = partial(self._on_sigint, main_task=task) - try: - signal.signal(signal.SIGINT, sigint_handler) - except ValueError: - # `signal.signal` may throw if `threading.main_thread` does - # not support signals (e.g. embedded interpreter with signals - # not registered - see gh-91880) - sigint_handler = None - else: - sigint_handler = None - - self._interrupt_count = 0 - try: - return self._loop.run_until_complete(task) - except exceptions.CancelledError: - if self._interrupt_count > 0: - uncancel = getattr(task, "uncancel", None) - if uncancel is not None and uncancel() == 0: - raise KeyboardInterrupt() - raise # CancelledError - finally: - if ( - sigint_handler is not None - and signal.getsignal(signal.SIGINT) is sigint_handler - ): - signal.signal(signal.SIGINT, signal.default_int_handler) - - def _lazy_init(self) -> None: - if self._state is _State.CLOSED: - raise RuntimeError("Runner is closed") - if self._state is _State.INITIALIZED: - return - if self._loop_factory is None: - self._loop = events.new_event_loop() - if not self._set_event_loop: - # Call set_event_loop only once to avoid calling - # attach_loop multiple times on child watchers - events.set_event_loop(self._loop) - self._set_event_loop = True - else: - self._loop = self._loop_factory() - if self._debug is not None: - self._loop.set_debug(self._debug) - self._context = contextvars.copy_context() - self._state = _State.INITIALIZED - - def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None: - self._interrupt_count += 1 - if self._interrupt_count == 1 and not main_task.done(): - main_task.cancel() - # wakeup loop if it is blocked by select() with long timeout - self._loop.call_soon_threadsafe(lambda: None) - return - raise KeyboardInterrupt() - - def _cancel_all_tasks(loop: AbstractEventLoop) -> None: - to_cancel = tasks.all_tasks(loop) - if not to_cancel: - return - - for task in to_cancel: - task.cancel() - - loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True)) - - for task in to_cancel: - if task.cancelled(): - continue - if task.exception() is not None: - loop.call_exception_handler( - { - "message": "unhandled exception during asyncio.run() shutdown", - "exception": task.exception(), - "task": task, - } - ) - - async def _shutdown_default_executor(loop: AbstractEventLoop) -> None: - """Schedule the shutdown of the default executor.""" - - def _do_shutdown(future: asyncio.futures.Future) -> None: - try: - loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined] - loop.call_soon_threadsafe(future.set_result, None) - except Exception as ex: - loop.call_soon_threadsafe(future.set_exception, ex) - - loop._executor_shutdown_called = True - if loop._default_executor is None: - return - future = loop.create_future() - thread = threading.Thread(target=_do_shutdown, args=(future,)) - thread.start() - try: - await future - finally: - thread.join() - - -T_Retval = TypeVar("T_Retval") -T_contra = TypeVar("T_contra", contravariant=True) -PosArgsT = TypeVarTuple("PosArgsT") -P = ParamSpec("P") - -_root_task: RunVar[asyncio.Task | None] = RunVar("_root_task") - - -def find_root_task() -> asyncio.Task: - root_task = _root_task.get(None) - if root_task is not None and not root_task.done(): - return root_task - - # Look for a task that has been started via run_until_complete() - for task in all_tasks(): - if task._callbacks and not task.done(): - callbacks = [cb for cb, context in task._callbacks] - for cb in callbacks: - if ( - cb is _run_until_complete_cb - or getattr(cb, "__module__", None) == "uvloop.loop" - ): - _root_task.set(task) - return task - - # Look up the topmost task in the AnyIO task tree, if possible - task = cast(asyncio.Task, current_task()) - state = _task_states.get(task) - if state: - cancel_scope = state.cancel_scope - while cancel_scope and cancel_scope._parent_scope is not None: - cancel_scope = cancel_scope._parent_scope - - if cancel_scope is not None: - return cast(asyncio.Task, cancel_scope._host_task) - - return task - - -def get_callable_name(func: Callable) -> str: - module = getattr(func, "__module__", None) - qualname = getattr(func, "__qualname__", None) - return ".".join([x for x in (module, qualname) if x]) - - -# -# Event loop -# - -_run_vars: WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = WeakKeyDictionary() - - -def _task_started(task: asyncio.Task) -> bool: - """Return ``True`` if the task has been started and has not finished.""" - # The task coro should never be None here, as we never add finished tasks to the - # task list - coro = task.get_coro() - assert coro is not None - try: - return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED) - except AttributeError: - # task coro is async_genenerator_asend https://bugs.python.org/issue37771 - raise Exception(f"Cannot determine if task {task} has started or not") from None - - -# -# Timeouts and cancellation -# - - -def is_anyio_cancellation(exc: CancelledError) -> bool: - # Sometimes third party frameworks catch a CancelledError and raise a new one, so as - # a workaround we have to look at the previous ones in __context__ too for a - # matching cancel message - while True: - if ( - exc.args - and isinstance(exc.args[0], str) - and exc.args[0].startswith("Cancelled by cancel scope ") - ): - return True - - if isinstance(exc.__context__, CancelledError): - exc = exc.__context__ - continue - - return False - - -class CancelScope(BaseCancelScope): - def __new__( - cls, *, deadline: float = math.inf, shield: bool = False - ) -> CancelScope: - return object.__new__(cls) - - def __init__(self, deadline: float = math.inf, shield: bool = False): - self._deadline = deadline - self._shield = shield - self._parent_scope: CancelScope | None = None - self._child_scopes: set[CancelScope] = set() - self._cancel_called = False - self._cancelled_caught = False - self._active = False - self._timeout_handle: asyncio.TimerHandle | None = None - self._cancel_handle: asyncio.Handle | None = None - self._tasks: set[asyncio.Task] = set() - self._host_task: asyncio.Task | None = None - if sys.version_info >= (3, 11): - self._pending_uncancellations: int | None = 0 - else: - self._pending_uncancellations = None - - def __enter__(self) -> CancelScope: - if self._active: - raise RuntimeError( - "Each CancelScope may only be used for a single 'with' block" - ) - - self._host_task = host_task = cast(asyncio.Task, current_task()) - self._tasks.add(host_task) - try: - task_state = _task_states[host_task] - except KeyError: - task_state = TaskState(None, self) - _task_states[host_task] = task_state - else: - self._parent_scope = task_state.cancel_scope - task_state.cancel_scope = self - if self._parent_scope is not None: - # If using an eager task factory, the parent scope may not even contain - # the host task - self._parent_scope._child_scopes.add(self) - self._parent_scope._tasks.discard(host_task) - - self._timeout() - self._active = True - - # Start cancelling the host task if the scope was cancelled before entering - if self._cancel_called: - self._deliver_cancellation(self) - - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool: - del exc_tb - - if not self._active: - raise RuntimeError("This cancel scope is not active") - if current_task() is not self._host_task: - raise RuntimeError( - "Attempted to exit cancel scope in a different task than it was " - "entered in" - ) - - assert self._host_task is not None - host_task_state = _task_states.get(self._host_task) - if host_task_state is None or host_task_state.cancel_scope is not self: - raise RuntimeError( - "Attempted to exit a cancel scope that isn't the current tasks's " - "current cancel scope" - ) - - try: - self._active = False - if self._timeout_handle: - self._timeout_handle.cancel() - self._timeout_handle = None - - self._tasks.remove(self._host_task) - if self._parent_scope is not None: - self._parent_scope._child_scopes.remove(self) - self._parent_scope._tasks.add(self._host_task) - - host_task_state.cancel_scope = self._parent_scope - - # Restart the cancellation effort in the closest visible, cancelled parent - # scope if necessary - self._restart_cancellation_in_parent() - - # We only swallow the exception iff it was an AnyIO CancelledError, either - # directly as exc_val or inside an exception group and there are no cancelled - # parent cancel scopes visible to us here - if self._cancel_called and not self._parent_cancellation_is_visible_to_us: - # For each level-cancel() call made on the host task, call uncancel() - while self._pending_uncancellations: - self._host_task.uncancel() - self._pending_uncancellations -= 1 - - # Update cancelled_caught and check for exceptions we must not swallow - cannot_swallow_exc_val = False - if exc_val is not None: - for exc in iterate_exceptions(exc_val): - if isinstance(exc, CancelledError) and is_anyio_cancellation( - exc - ): - self._cancelled_caught = True - else: - cannot_swallow_exc_val = True - - return self._cancelled_caught and not cannot_swallow_exc_val - else: - if self._pending_uncancellations: - assert self._parent_scope is not None - assert self._parent_scope._pending_uncancellations is not None - self._parent_scope._pending_uncancellations += ( - self._pending_uncancellations - ) - self._pending_uncancellations = 0 - - return False - finally: - self._host_task = None - del exc_val - - @property - def _effectively_cancelled(self) -> bool: - cancel_scope: CancelScope | None = self - while cancel_scope is not None: - if cancel_scope._cancel_called: - return True - - if cancel_scope.shield: - return False - - cancel_scope = cancel_scope._parent_scope - - return False - - @property - def _parent_cancellation_is_visible_to_us(self) -> bool: - return ( - self._parent_scope is not None - and not self.shield - and self._parent_scope._effectively_cancelled - ) - - def _timeout(self) -> None: - if self._deadline != math.inf: - loop = get_running_loop() - if loop.time() >= self._deadline: - self.cancel() - else: - self._timeout_handle = loop.call_at(self._deadline, self._timeout) - - def _deliver_cancellation(self, origin: CancelScope) -> bool: - """ - Deliver cancellation to directly contained tasks and nested cancel scopes. - - Schedule another run at the end if we still have tasks eligible for - cancellation. - - :param origin: the cancel scope that originated the cancellation - :return: ``True`` if the delivery needs to be retried on the next cycle - - """ - should_retry = False - current = current_task() - for task in self._tasks: - should_retry = True - if task._must_cancel: # type: ignore[attr-defined] - continue - - # The task is eligible for cancellation if it has started - if task is not current and (task is self._host_task or _task_started(task)): - waiter = task._fut_waiter # type: ignore[attr-defined] - if not isinstance(waiter, asyncio.Future) or not waiter.done(): - task.cancel(f"Cancelled by cancel scope {id(origin):x}") - if ( - task is origin._host_task - and origin._pending_uncancellations is not None - ): - origin._pending_uncancellations += 1 - - # Deliver cancellation to child scopes that aren't shielded or running their own - # cancellation callbacks - for scope in self._child_scopes: - if not scope._shield and not scope.cancel_called: - should_retry = scope._deliver_cancellation(origin) or should_retry - - # Schedule another callback if there are still tasks left - if origin is self: - if should_retry: - self._cancel_handle = get_running_loop().call_soon( - self._deliver_cancellation, origin - ) - else: - self._cancel_handle = None - - return should_retry - - def _restart_cancellation_in_parent(self) -> None: - """ - Restart the cancellation effort in the closest directly cancelled parent scope. - - """ - scope = self._parent_scope - while scope is not None: - if scope._cancel_called: - if scope._cancel_handle is None: - scope._deliver_cancellation(scope) - - break - - # No point in looking beyond any shielded scope - if scope._shield: - break - - scope = scope._parent_scope - - def cancel(self) -> None: - if not self._cancel_called: - if self._timeout_handle: - self._timeout_handle.cancel() - self._timeout_handle = None - - self._cancel_called = True - if self._host_task is not None: - self._deliver_cancellation(self) - - @property - def deadline(self) -> float: - return self._deadline - - @deadline.setter - def deadline(self, value: float) -> None: - self._deadline = float(value) - if self._timeout_handle is not None: - self._timeout_handle.cancel() - self._timeout_handle = None - - if self._active and not self._cancel_called: - self._timeout() - - @property - def cancel_called(self) -> bool: - return self._cancel_called - - @property - def cancelled_caught(self) -> bool: - return self._cancelled_caught - - @property - def shield(self) -> bool: - return self._shield - - @shield.setter - def shield(self, value: bool) -> None: - if self._shield != value: - self._shield = value - if not value: - self._restart_cancellation_in_parent() - - -# -# Task states -# - - -class TaskState: - """ - Encapsulates auxiliary task information that cannot be added to the Task instance - itself because there are no guarantees about its implementation. - """ - - __slots__ = "parent_id", "cancel_scope", "__weakref__" - - def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): - self.parent_id = parent_id - self.cancel_scope = cancel_scope - - -_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() - - -# -# Task groups -# - - -class _AsyncioTaskStatus(abc.TaskStatus): - def __init__(self, future: asyncio.Future, parent_id: int): - self._future = future - self._parent_id = parent_id - - def started(self, value: T_contra | None = None) -> None: - try: - self._future.set_result(value) - except asyncio.InvalidStateError: - if not self._future.cancelled(): - raise RuntimeError( - "called 'started' twice on the same task status" - ) from None - - task = cast(asyncio.Task, current_task()) - _task_states[task].parent_id = self._parent_id - - -if sys.version_info >= (3, 12): - _eager_task_factory_code: CodeType | None = asyncio.eager_task_factory.__code__ -else: - _eager_task_factory_code = None - - -class TaskGroup(abc.TaskGroup): - def __init__(self) -> None: - self.cancel_scope: CancelScope = CancelScope() - self._active = False - self._exceptions: list[BaseException] = [] - self._tasks: set[asyncio.Task] = set() - self._on_completed_fut: asyncio.Future[None] | None = None - - async def __aenter__(self) -> TaskGroup: - self.cancel_scope.__enter__() - self._active = True - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - try: - if exc_val is not None: - self.cancel_scope.cancel() - if not isinstance(exc_val, CancelledError): - self._exceptions.append(exc_val) - - loop = get_running_loop() - try: - if self._tasks: - with CancelScope() as wait_scope: - while self._tasks: - self._on_completed_fut = loop.create_future() - - try: - await self._on_completed_fut - except CancelledError as exc: - # Shield the scope against further cancellation attempts, - # as they're not productive (#695) - wait_scope.shield = True - self.cancel_scope.cancel() - - # Set exc_val from the cancellation exception if it was - # previously unset. However, we should not replace a native - # cancellation exception with one raise by a cancel scope. - if exc_val is None or ( - isinstance(exc_val, CancelledError) - and not is_anyio_cancellation(exc) - ): - exc_val = exc - - self._on_completed_fut = None - else: - # If there are no child tasks to wait on, run at least one checkpoint - # anyway - await AsyncIOBackend.cancel_shielded_checkpoint() - - self._active = False - if self._exceptions: - # The exception that got us here should already have been - # added to self._exceptions so it's ok to break exception - # chaining and avoid adding a "During handling of above..." - # for each nesting level. - raise BaseExceptionGroup( - "unhandled errors in a TaskGroup", self._exceptions - ) from None - elif exc_val: - raise exc_val - except BaseException as exc: - if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__): - return True - - raise - - return self.cancel_scope.__exit__(exc_type, exc_val, exc_tb) - finally: - del exc_val, exc_tb, self._exceptions - - def _spawn( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], - args: tuple[Unpack[PosArgsT]], - name: object, - task_status_future: asyncio.Future | None = None, - ) -> asyncio.Task: - def task_done(_task: asyncio.Task) -> None: - task_state = _task_states[_task] - assert task_state.cancel_scope is not None - assert _task in task_state.cancel_scope._tasks - task_state.cancel_scope._tasks.remove(_task) - self._tasks.remove(task) - del _task_states[_task] - - if self._on_completed_fut is not None and not self._tasks: - try: - self._on_completed_fut.set_result(None) - except asyncio.InvalidStateError: - pass - - try: - exc = _task.exception() - except CancelledError as e: - while isinstance(e.__context__, CancelledError): - e = e.__context__ - - exc = e - - if exc is not None: - # The future can only be in the cancelled state if the host task was - # cancelled, so return immediately instead of adding one more - # CancelledError to the exceptions list - if task_status_future is not None and task_status_future.cancelled(): - return - - if task_status_future is None or task_status_future.done(): - if not isinstance(exc, CancelledError): - self._exceptions.append(exc) - - if not self.cancel_scope._effectively_cancelled: - self.cancel_scope.cancel() - else: - task_status_future.set_exception(exc) - elif task_status_future is not None and not task_status_future.done(): - task_status_future.set_exception( - RuntimeError("Child exited without calling task_status.started()") - ) - - if not self._active: - raise RuntimeError( - "This task group is not active; no new tasks can be started." - ) - - kwargs = {} - if task_status_future: - parent_id = id(current_task()) - kwargs["task_status"] = _AsyncioTaskStatus( - task_status_future, id(self.cancel_scope._host_task) - ) - else: - parent_id = id(self.cancel_scope._host_task) - - coro = func(*args, **kwargs) - if not iscoroutine(coro): - prefix = f"{func.__module__}." if hasattr(func, "__module__") else "" - raise TypeError( - f"Expected {prefix}{func.__qualname__}() to return a coroutine, but " - f"the return value ({coro!r}) is not a coroutine object" - ) - - name = get_callable_name(func) if name is None else str(name) - loop = asyncio.get_running_loop() - if ( - (factory := loop.get_task_factory()) - and getattr(factory, "__code__", None) is _eager_task_factory_code - and (closure := getattr(factory, "__closure__", None)) - ): - custom_task_constructor = closure[0].cell_contents - task = custom_task_constructor(coro, loop=loop, name=name) - else: - task = create_task(coro, name=name) - - # Make the spawned task inherit the task group's cancel scope - _task_states[task] = TaskState( - parent_id=parent_id, cancel_scope=self.cancel_scope - ) - self.cancel_scope._tasks.add(task) - self._tasks.add(task) - task.add_done_callback(task_done) - return task - - def start_soon( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], - *args: Unpack[PosArgsT], - name: object = None, - ) -> None: - self._spawn(func, args, name) - - async def start( - self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None - ) -> Any: - future: asyncio.Future = asyncio.Future() - task = self._spawn(func, args, name, future) - - # If the task raises an exception after sending a start value without a switch - # point between, the task group is cancelled and this method never proceeds to - # process the completed future. That's why we have to have a shielded cancel - # scope here. - try: - return await future - except CancelledError: - # Cancel the task and wait for it to exit before returning - task.cancel() - with CancelScope(shield=True), suppress(CancelledError): - await task - - raise - - -# -# Threads -# - -_Retval_Queue_Type = tuple[Optional[T_Retval], Optional[BaseException]] - - -class WorkerThread(Thread): - MAX_IDLE_TIME = 10 # seconds - - def __init__( - self, - root_task: asyncio.Task, - workers: set[WorkerThread], - idle_workers: deque[WorkerThread], - ): - super().__init__(name="AnyIO worker thread") - self.root_task = root_task - self.workers = workers - self.idle_workers = idle_workers - self.loop = root_task._loop - self.queue: Queue[ - tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None - ] = Queue(2) - self.idle_since = AsyncIOBackend.current_time() - self.stopping = False - - def _report_result( - self, future: asyncio.Future, result: Any, exc: BaseException | None - ) -> None: - self.idle_since = AsyncIOBackend.current_time() - if not self.stopping: - self.idle_workers.append(self) - - if not future.cancelled(): - if exc is not None: - if isinstance(exc, StopIteration): - new_exc = RuntimeError("coroutine raised StopIteration") - new_exc.__cause__ = exc - exc = new_exc - - future.set_exception(exc) - else: - future.set_result(result) - - def run(self) -> None: - with claim_worker_thread(AsyncIOBackend, self.loop): - while True: - item = self.queue.get() - if item is None: - # Shutdown command received - return - - context, func, args, future, cancel_scope = item - if not future.cancelled(): - result = None - exception: BaseException | None = None - threadlocals.current_cancel_scope = cancel_scope - try: - result = context.run(func, *args) - except BaseException as exc: - exception = exc - finally: - del threadlocals.current_cancel_scope - - if not self.loop.is_closed(): - self.loop.call_soon_threadsafe( - self._report_result, future, result, exception - ) - - del result, exception - - self.queue.task_done() - del item, context, func, args, future, cancel_scope - - def stop(self, f: asyncio.Task | None = None) -> None: - self.stopping = True - self.queue.put_nowait(None) - self.workers.discard(self) - try: - self.idle_workers.remove(self) - except ValueError: - pass - - -_threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar( - "_threadpool_idle_workers" -) -_threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") - - -class BlockingPortal(abc.BlockingPortal): - def __new__(cls) -> BlockingPortal: - return object.__new__(cls) - - def __init__(self) -> None: - super().__init__() - self._loop = get_running_loop() - - def _spawn_task_from_thread( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - name: object, - future: Future[T_Retval], - ) -> None: - AsyncIOBackend.run_sync_from_thread( - partial(self._task_group.start_soon, name=name), - (self._call_func, func, args, kwargs, future), - self._loop, - ) - - -# -# Subprocesses -# - - -@dataclass(eq=False) -class StreamReaderWrapper(abc.ByteReceiveStream): - _stream: asyncio.StreamReader - - async def receive(self, max_bytes: int = 65536) -> bytes: - data = await self._stream.read(max_bytes) - if data: - return data - else: - raise EndOfStream - - async def aclose(self) -> None: - self._stream.set_exception(ClosedResourceError()) - await AsyncIOBackend.checkpoint() - - -@dataclass(eq=False) -class StreamWriterWrapper(abc.ByteSendStream): - _stream: asyncio.StreamWriter - - async def send(self, item: bytes) -> None: - self._stream.write(item) - await self._stream.drain() - - async def aclose(self) -> None: - self._stream.close() - await AsyncIOBackend.checkpoint() - - -@dataclass(eq=False) -class Process(abc.Process): - _process: asyncio.subprocess.Process - _stdin: StreamWriterWrapper | None - _stdout: StreamReaderWrapper | None - _stderr: StreamReaderWrapper | None - - async def aclose(self) -> None: - with CancelScope(shield=True) as scope: - if self._stdin: - await self._stdin.aclose() - if self._stdout: - await self._stdout.aclose() - if self._stderr: - await self._stderr.aclose() - - scope.shield = False - try: - await self.wait() - except BaseException: - scope.shield = True - self.kill() - await self.wait() - raise - - async def wait(self) -> int: - return await self._process.wait() - - def terminate(self) -> None: - self._process.terminate() - - def kill(self) -> None: - self._process.kill() - - def send_signal(self, signal: int) -> None: - self._process.send_signal(signal) - - @property - def pid(self) -> int: - return self._process.pid - - @property - def returncode(self) -> int | None: - return self._process.returncode - - @property - def stdin(self) -> abc.ByteSendStream | None: - return self._stdin - - @property - def stdout(self) -> abc.ByteReceiveStream | None: - return self._stdout - - @property - def stderr(self) -> abc.ByteReceiveStream | None: - return self._stderr - - -def _forcibly_shutdown_process_pool_on_exit( - workers: set[Process], _task: object -) -> None: - """ - Forcibly shuts down worker processes belonging to this event loop.""" - child_watcher: asyncio.AbstractChildWatcher | None = None - if sys.version_info < (3, 12): - try: - child_watcher = asyncio.get_event_loop_policy().get_child_watcher() - except NotImplementedError: - pass - - # Close as much as possible (w/o async/await) to avoid warnings - for process in workers: - if process.returncode is None: - continue - - process._stdin._stream._transport.close() # type: ignore[union-attr] - process._stdout._stream._transport.close() # type: ignore[union-attr] - process._stderr._stream._transport.close() # type: ignore[union-attr] - process.kill() - if child_watcher: - child_watcher.remove_child_handler(process.pid) - - -async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None: - """ - Shuts down worker processes belonging to this event loop. - - NOTE: this only works when the event loop was started using asyncio.run() or - anyio.run(). - - """ - process: abc.Process - try: - await sleep(math.inf) - except asyncio.CancelledError: - for process in workers: - if process.returncode is None: - process.kill() - - for process in workers: - await process.aclose() - - -# -# Sockets and networking -# - - -class StreamProtocol(asyncio.Protocol): - read_queue: deque[bytes] - read_event: asyncio.Event - write_event: asyncio.Event - exception: Exception | None = None - is_at_eof: bool = False - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self.read_queue = deque() - self.read_event = asyncio.Event() - self.write_event = asyncio.Event() - self.write_event.set() - cast(asyncio.Transport, transport).set_write_buffer_limits(0) - - def connection_lost(self, exc: Exception | None) -> None: - if exc: - self.exception = BrokenResourceError() - self.exception.__cause__ = exc - - self.read_event.set() - self.write_event.set() - - def data_received(self, data: bytes) -> None: - # ProactorEventloop sometimes sends bytearray instead of bytes - self.read_queue.append(bytes(data)) - self.read_event.set() - - def eof_received(self) -> bool | None: - self.is_at_eof = True - self.read_event.set() - return True - - def pause_writing(self) -> None: - self.write_event = asyncio.Event() - - def resume_writing(self) -> None: - self.write_event.set() - - -class DatagramProtocol(asyncio.DatagramProtocol): - read_queue: deque[tuple[bytes, IPSockAddrType]] - read_event: asyncio.Event - write_event: asyncio.Event - exception: Exception | None = None - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - self.read_queue = deque(maxlen=100) # arbitrary value - self.read_event = asyncio.Event() - self.write_event = asyncio.Event() - self.write_event.set() - - def connection_lost(self, exc: Exception | None) -> None: - self.read_event.set() - self.write_event.set() - - def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None: - addr = convert_ipv6_sockaddr(addr) - self.read_queue.append((data, addr)) - self.read_event.set() - - def error_received(self, exc: Exception) -> None: - self.exception = exc - - def pause_writing(self) -> None: - self.write_event.clear() - - def resume_writing(self) -> None: - self.write_event.set() - - -class SocketStream(abc.SocketStream): - def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol): - self._transport = transport - self._protocol = protocol - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - self._closed = False - - @property - def _raw_socket(self) -> socket.socket: - return self._transport.get_extra_info("socket") - - async def receive(self, max_bytes: int = 65536) -> bytes: - with self._receive_guard: - if ( - not self._protocol.read_event.is_set() - and not self._transport.is_closing() - and not self._protocol.is_at_eof - ): - self._transport.resume_reading() - await self._protocol.read_event.wait() - self._transport.pause_reading() - else: - await AsyncIOBackend.checkpoint() - - try: - chunk = self._protocol.read_queue.popleft() - except IndexError: - if self._closed: - raise ClosedResourceError from None - elif self._protocol.exception: - raise self._protocol.exception from None - else: - raise EndOfStream from None - - if len(chunk) > max_bytes: - # Split the oversized chunk - chunk, leftover = chunk[:max_bytes], chunk[max_bytes:] - self._protocol.read_queue.appendleft(leftover) - - # If the read queue is empty, clear the flag so that the next call will - # block until data is available - if not self._protocol.read_queue: - self._protocol.read_event.clear() - - return chunk - - async def send(self, item: bytes) -> None: - with self._send_guard: - await AsyncIOBackend.checkpoint() - - if self._closed: - raise ClosedResourceError - elif self._protocol.exception is not None: - raise self._protocol.exception - - try: - self._transport.write(item) - except RuntimeError as exc: - if self._transport.is_closing(): - raise BrokenResourceError from exc - else: - raise - - await self._protocol.write_event.wait() - - async def send_eof(self) -> None: - try: - self._transport.write_eof() - except OSError: - pass - - async def aclose(self) -> None: - if not self._transport.is_closing(): - self._closed = True - try: - self._transport.write_eof() - except OSError: - pass - - self._transport.close() - await sleep(0) - self._transport.abort() - - -class _RawSocketMixin: - _receive_future: asyncio.Future | None = None - _send_future: asyncio.Future | None = None - _closing = False - - def __init__(self, raw_socket: socket.socket): - self.__raw_socket = raw_socket - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - @property - def _raw_socket(self) -> socket.socket: - return self.__raw_socket - - def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: - def callback(f: object) -> None: - del self._receive_future - loop.remove_reader(self.__raw_socket) - - f = self._receive_future = asyncio.Future() - loop.add_reader(self.__raw_socket, f.set_result, None) - f.add_done_callback(callback) - return f - - def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: - def callback(f: object) -> None: - del self._send_future - loop.remove_writer(self.__raw_socket) - - f = self._send_future = asyncio.Future() - loop.add_writer(self.__raw_socket, f.set_result, None) - f.add_done_callback(callback) - return f - - async def aclose(self) -> None: - if not self._closing: - self._closing = True - if self.__raw_socket.fileno() != -1: - self.__raw_socket.close() - - if self._receive_future: - self._receive_future.set_result(None) - if self._send_future: - self._send_future.set_result(None) - - -class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream): - async def send_eof(self) -> None: - with self._send_guard: - self._raw_socket.shutdown(socket.SHUT_WR) - - async def receive(self, max_bytes: int = 65536) -> bytes: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._receive_guard: - while True: - try: - data = self._raw_socket.recv(max_bytes) - except BlockingIOError: - await self._wait_until_readable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - if not data: - raise EndOfStream - - return data - - async def send(self, item: bytes) -> None: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._send_guard: - view = memoryview(item) - while view: - try: - bytes_sent = self._raw_socket.send(view) - except BlockingIOError: - await self._wait_until_writable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - view = view[bytes_sent:] - - async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: - if not isinstance(msglen, int) or msglen < 0: - raise ValueError("msglen must be a non-negative integer") - if not isinstance(maxfds, int) or maxfds < 1: - raise ValueError("maxfds must be a positive integer") - - loop = get_running_loop() - fds = array.array("i") - await AsyncIOBackend.checkpoint() - with self._receive_guard: - while True: - try: - message, ancdata, flags, addr = self._raw_socket.recvmsg( - msglen, socket.CMSG_LEN(maxfds * fds.itemsize) - ) - except BlockingIOError: - await self._wait_until_readable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - if not message and not ancdata: - raise EndOfStream - - break - - for cmsg_level, cmsg_type, cmsg_data in ancdata: - if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: - raise RuntimeError( - f"Received unexpected ancillary data; message = {message!r}, " - f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" - ) - - fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) - - return message, list(fds) - - async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: - if not message: - raise ValueError("message must not be empty") - if not fds: - raise ValueError("fds must not be empty") - - loop = get_running_loop() - filenos: list[int] = [] - for fd in fds: - if isinstance(fd, int): - filenos.append(fd) - elif isinstance(fd, IOBase): - filenos.append(fd.fileno()) - - fdarray = array.array("i", filenos) - await AsyncIOBackend.checkpoint() - with self._send_guard: - while True: - try: - # The ignore can be removed after mypy picks up - # https://github.com/python/typeshed/pull/5545 - self._raw_socket.sendmsg( - [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)] - ) - break - except BlockingIOError: - await self._wait_until_writable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - - -class TCPSocketListener(abc.SocketListener): - _accept_scope: CancelScope | None = None - _closed = False - - def __init__(self, raw_socket: socket.socket): - self.__raw_socket = raw_socket - self._loop = cast(asyncio.BaseEventLoop, get_running_loop()) - self._accept_guard = ResourceGuard("accepting connections from") - - @property - def _raw_socket(self) -> socket.socket: - return self.__raw_socket - - async def accept(self) -> abc.SocketStream: - if self._closed: - raise ClosedResourceError - - with self._accept_guard: - await AsyncIOBackend.checkpoint() - with CancelScope() as self._accept_scope: - try: - client_sock, _addr = await self._loop.sock_accept(self._raw_socket) - except asyncio.CancelledError: - # Workaround for https://bugs.python.org/issue41317 - try: - self._loop.remove_reader(self._raw_socket) - except (ValueError, NotImplementedError): - pass - - if self._closed: - raise ClosedResourceError from None - - raise - finally: - self._accept_scope = None - - client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - transport, protocol = await self._loop.connect_accepted_socket( - StreamProtocol, client_sock - ) - return SocketStream(transport, protocol) - - async def aclose(self) -> None: - if self._closed: - return - - self._closed = True - if self._accept_scope: - # Workaround for https://bugs.python.org/issue41317 - try: - self._loop.remove_reader(self._raw_socket) - except (ValueError, NotImplementedError): - pass - - self._accept_scope.cancel() - await sleep(0) - - self._raw_socket.close() - - -class UNIXSocketListener(abc.SocketListener): - def __init__(self, raw_socket: socket.socket): - self.__raw_socket = raw_socket - self._loop = get_running_loop() - self._accept_guard = ResourceGuard("accepting connections from") - self._closed = False - - async def accept(self) -> abc.SocketStream: - await AsyncIOBackend.checkpoint() - with self._accept_guard: - while True: - try: - client_sock, _ = self.__raw_socket.accept() - client_sock.setblocking(False) - return UNIXSocketStream(client_sock) - except BlockingIOError: - f: asyncio.Future = asyncio.Future() - self._loop.add_reader(self.__raw_socket, f.set_result, None) - f.add_done_callback( - lambda _: self._loop.remove_reader(self.__raw_socket) - ) - await f - except OSError as exc: - if self._closed: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - - async def aclose(self) -> None: - self._closed = True - self.__raw_socket.close() - - @property - def _raw_socket(self) -> socket.socket: - return self.__raw_socket - - -class UDPSocket(abc.UDPSocket): - def __init__( - self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol - ): - self._transport = transport - self._protocol = protocol - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - self._closed = False - - @property - def _raw_socket(self) -> socket.socket: - return self._transport.get_extra_info("socket") - - async def aclose(self) -> None: - if not self._transport.is_closing(): - self._closed = True - self._transport.close() - - async def receive(self) -> tuple[bytes, IPSockAddrType]: - with self._receive_guard: - await AsyncIOBackend.checkpoint() - - # If the buffer is empty, ask for more data - if not self._protocol.read_queue and not self._transport.is_closing(): - self._protocol.read_event.clear() - await self._protocol.read_event.wait() - - try: - return self._protocol.read_queue.popleft() - except IndexError: - if self._closed: - raise ClosedResourceError from None - else: - raise BrokenResourceError from None - - async def send(self, item: UDPPacketType) -> None: - with self._send_guard: - await AsyncIOBackend.checkpoint() - await self._protocol.write_event.wait() - if self._closed: - raise ClosedResourceError - elif self._transport.is_closing(): - raise BrokenResourceError - else: - self._transport.sendto(*item) - - -class ConnectedUDPSocket(abc.ConnectedUDPSocket): - def __init__( - self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol - ): - self._transport = transport - self._protocol = protocol - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - self._closed = False - - @property - def _raw_socket(self) -> socket.socket: - return self._transport.get_extra_info("socket") - - async def aclose(self) -> None: - if not self._transport.is_closing(): - self._closed = True - self._transport.close() - - async def receive(self) -> bytes: - with self._receive_guard: - await AsyncIOBackend.checkpoint() - - # If the buffer is empty, ask for more data - if not self._protocol.read_queue and not self._transport.is_closing(): - self._protocol.read_event.clear() - await self._protocol.read_event.wait() - - try: - packet = self._protocol.read_queue.popleft() - except IndexError: - if self._closed: - raise ClosedResourceError from None - else: - raise BrokenResourceError from None - - return packet[0] - - async def send(self, item: bytes) -> None: - with self._send_guard: - await AsyncIOBackend.checkpoint() - await self._protocol.write_event.wait() - if self._closed: - raise ClosedResourceError - elif self._transport.is_closing(): - raise BrokenResourceError - else: - self._transport.sendto(item) - - -class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket): - async def receive(self) -> UNIXDatagramPacketType: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._receive_guard: - while True: - try: - data = self._raw_socket.recvfrom(65536) - except BlockingIOError: - await self._wait_until_readable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - return data - - async def send(self, item: UNIXDatagramPacketType) -> None: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._send_guard: - while True: - try: - self._raw_socket.sendto(*item) - except BlockingIOError: - await self._wait_until_writable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - return - - -class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket): - async def receive(self) -> bytes: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._receive_guard: - while True: - try: - data = self._raw_socket.recv(65536) - except BlockingIOError: - await self._wait_until_readable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - return data - - async def send(self, item: bytes) -> None: - loop = get_running_loop() - await AsyncIOBackend.checkpoint() - with self._send_guard: - while True: - try: - self._raw_socket.send(item) - except BlockingIOError: - await self._wait_until_writable(loop) - except OSError as exc: - if self._closing: - raise ClosedResourceError from None - else: - raise BrokenResourceError from exc - else: - return - - -_read_events: RunVar[dict[int, asyncio.Event]] = RunVar("read_events") -_write_events: RunVar[dict[int, asyncio.Event]] = RunVar("write_events") - - -# -# Synchronization -# - - -class Event(BaseEvent): - def __new__(cls) -> Event: - return object.__new__(cls) - - def __init__(self) -> None: - self._event = asyncio.Event() - - def set(self) -> None: - self._event.set() - - def is_set(self) -> bool: - return self._event.is_set() - - async def wait(self) -> None: - if self.is_set(): - await AsyncIOBackend.checkpoint() - else: - await self._event.wait() - - def statistics(self) -> EventStatistics: - return EventStatistics(len(self._event._waiters)) - - -class Lock(BaseLock): - def __new__(cls, *, fast_acquire: bool = False) -> Lock: - return object.__new__(cls) - - def __init__(self, *, fast_acquire: bool = False) -> None: - self._fast_acquire = fast_acquire - self._owner_task: asyncio.Task | None = None - self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque() - - async def acquire(self) -> None: - task = cast(asyncio.Task, current_task()) - if self._owner_task is None and not self._waiters: - await AsyncIOBackend.checkpoint_if_cancelled() - self._owner_task = task - - # Unless on the "fast path", yield control of the event loop so that other - # tasks can run too - if not self._fast_acquire: - try: - await AsyncIOBackend.cancel_shielded_checkpoint() - except CancelledError: - self.release() - raise - - return - - if self._owner_task == task: - raise RuntimeError("Attempted to acquire an already held Lock") - - fut: asyncio.Future[None] = asyncio.Future() - item = task, fut - self._waiters.append(item) - try: - await fut - except CancelledError: - self._waiters.remove(item) - if self._owner_task is task: - self.release() - - raise - - self._waiters.remove(item) - - def acquire_nowait(self) -> None: - task = cast(asyncio.Task, current_task()) - if self._owner_task is None and not self._waiters: - self._owner_task = task - return - - if self._owner_task is task: - raise RuntimeError("Attempted to acquire an already held Lock") - - raise WouldBlock - - def locked(self) -> bool: - return self._owner_task is not None - - def release(self) -> None: - if self._owner_task != current_task(): - raise RuntimeError("The current task is not holding this lock") - - for task, fut in self._waiters: - if not fut.cancelled(): - self._owner_task = task - fut.set_result(None) - return - - self._owner_task = None - - def statistics(self) -> LockStatistics: - task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None - return LockStatistics(self.locked(), task_info, len(self._waiters)) - - -class Semaphore(BaseSemaphore): - def __new__( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> Semaphore: - return object.__new__(cls) - - def __init__( - self, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ): - super().__init__(initial_value, max_value=max_value) - self._value = initial_value - self._max_value = max_value - self._fast_acquire = fast_acquire - self._waiters: deque[asyncio.Future[None]] = deque() - - async def acquire(self) -> None: - if self._value > 0 and not self._waiters: - await AsyncIOBackend.checkpoint_if_cancelled() - self._value -= 1 - - # Unless on the "fast path", yield control of the event loop so that other - # tasks can run too - if not self._fast_acquire: - try: - await AsyncIOBackend.cancel_shielded_checkpoint() - except CancelledError: - self.release() - raise - - return - - fut: asyncio.Future[None] = asyncio.Future() - self._waiters.append(fut) - try: - await fut - except CancelledError: - try: - self._waiters.remove(fut) - except ValueError: - self.release() - - raise - - def acquire_nowait(self) -> None: - if self._value == 0: - raise WouldBlock - - self._value -= 1 - - def release(self) -> None: - if self._max_value is not None and self._value == self._max_value: - raise ValueError("semaphore released too many times") - - for fut in self._waiters: - if not fut.cancelled(): - fut.set_result(None) - self._waiters.remove(fut) - return - - self._value += 1 - - @property - def value(self) -> int: - return self._value - - @property - def max_value(self) -> int | None: - return self._max_value - - def statistics(self) -> SemaphoreStatistics: - return SemaphoreStatistics(len(self._waiters)) - - -class CapacityLimiter(BaseCapacityLimiter): - _total_tokens: float = 0 - - def __new__(cls, total_tokens: float) -> CapacityLimiter: - return object.__new__(cls) - - def __init__(self, total_tokens: float): - self._borrowers: set[Any] = set() - self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict() - self.total_tokens = total_tokens - - async def __aenter__(self) -> None: - await self.acquire() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.release() - - @property - def total_tokens(self) -> float: - return self._total_tokens - - @total_tokens.setter - def total_tokens(self, value: float) -> None: - if not isinstance(value, int) and not math.isinf(value): - raise TypeError("total_tokens must be an int or math.inf") - if value < 1: - raise ValueError("total_tokens must be >= 1") - - waiters_to_notify = max(value - self._total_tokens, 0) - self._total_tokens = value - - # Notify waiting tasks that they have acquired the limiter - while self._wait_queue and waiters_to_notify: - event = self._wait_queue.popitem(last=False)[1] - event.set() - waiters_to_notify -= 1 - - @property - def borrowed_tokens(self) -> int: - return len(self._borrowers) - - @property - def available_tokens(self) -> float: - return self._total_tokens - len(self._borrowers) - - def acquire_nowait(self) -> None: - self.acquire_on_behalf_of_nowait(current_task()) - - def acquire_on_behalf_of_nowait(self, borrower: object) -> None: - if borrower in self._borrowers: - raise RuntimeError( - "this borrower is already holding one of this CapacityLimiter's tokens" - ) - - if self._wait_queue or len(self._borrowers) >= self._total_tokens: - raise WouldBlock - - self._borrowers.add(borrower) - - async def acquire(self) -> None: - return await self.acquire_on_behalf_of(current_task()) - - async def acquire_on_behalf_of(self, borrower: object) -> None: - await AsyncIOBackend.checkpoint_if_cancelled() - try: - self.acquire_on_behalf_of_nowait(borrower) - except WouldBlock: - event = asyncio.Event() - self._wait_queue[borrower] = event - try: - await event.wait() - except BaseException: - self._wait_queue.pop(borrower, None) - raise - - self._borrowers.add(borrower) - else: - try: - await AsyncIOBackend.cancel_shielded_checkpoint() - except BaseException: - self.release() - raise - - def release(self) -> None: - self.release_on_behalf_of(current_task()) - - def release_on_behalf_of(self, borrower: object) -> None: - try: - self._borrowers.remove(borrower) - except KeyError: - raise RuntimeError( - "this borrower isn't holding any of this CapacityLimiter's tokens" - ) from None - - # Notify the next task in line if this limiter has free capacity now - if self._wait_queue and len(self._borrowers) < self._total_tokens: - event = self._wait_queue.popitem(last=False)[1] - event.set() - - def statistics(self) -> CapacityLimiterStatistics: - return CapacityLimiterStatistics( - self.borrowed_tokens, - self.total_tokens, - tuple(self._borrowers), - len(self._wait_queue), - ) - - -_default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter") - - -# -# Operating system signals -# - - -class _SignalReceiver: - def __init__(self, signals: tuple[Signals, ...]): - self._signals = signals - self._loop = get_running_loop() - self._signal_queue: deque[Signals] = deque() - self._future: asyncio.Future = asyncio.Future() - self._handled_signals: set[Signals] = set() - - def _deliver(self, signum: Signals) -> None: - self._signal_queue.append(signum) - if not self._future.done(): - self._future.set_result(None) - - def __enter__(self) -> _SignalReceiver: - for sig in set(self._signals): - self._loop.add_signal_handler(sig, self._deliver, sig) - self._handled_signals.add(sig) - - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - for sig in self._handled_signals: - self._loop.remove_signal_handler(sig) - - def __aiter__(self) -> _SignalReceiver: - return self - - async def __anext__(self) -> Signals: - await AsyncIOBackend.checkpoint() - if not self._signal_queue: - self._future = asyncio.Future() - await self._future - - return self._signal_queue.popleft() - - -# -# Testing and debugging -# - - -class AsyncIOTaskInfo(TaskInfo): - def __init__(self, task: asyncio.Task): - task_state = _task_states.get(task) - if task_state is None: - parent_id = None - else: - parent_id = task_state.parent_id - - coro = task.get_coro() - assert coro is not None, "created TaskInfo from a completed Task" - super().__init__(id(task), parent_id, task.get_name(), coro) - self._task = weakref.ref(task) - - def has_pending_cancellation(self) -> bool: - if not (task := self._task()): - # If the task isn't around anymore, it won't have a pending cancellation - return False - - if task._must_cancel: # type: ignore[attr-defined] - return True - elif ( - isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined] - and task._fut_waiter.cancelled() # type: ignore[attr-defined] - ): - return True - - if task_state := _task_states.get(task): - if cancel_scope := task_state.cancel_scope: - return cancel_scope._effectively_cancelled - - return False - - -class TestRunner(abc.TestRunner): - _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]] - - def __init__( - self, - *, - debug: bool | None = None, - use_uvloop: bool = False, - loop_factory: Callable[[], AbstractEventLoop] | None = None, - ) -> None: - if use_uvloop and loop_factory is None: - import uvloop - - loop_factory = uvloop.new_event_loop - - self._runner = Runner(debug=debug, loop_factory=loop_factory) - self._exceptions: list[BaseException] = [] - self._runner_task: asyncio.Task | None = None - - def __enter__(self) -> TestRunner: - self._runner.__enter__() - self.get_loop().set_exception_handler(self._exception_handler) - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self._runner.__exit__(exc_type, exc_val, exc_tb) - - def get_loop(self) -> AbstractEventLoop: - return self._runner.get_loop() - - def _exception_handler( - self, loop: asyncio.AbstractEventLoop, context: dict[str, Any] - ) -> None: - if isinstance(context.get("exception"), Exception): - self._exceptions.append(context["exception"]) - else: - loop.default_exception_handler(context) - - def _raise_async_exceptions(self) -> None: - # Re-raise any exceptions raised in asynchronous callbacks - if self._exceptions: - exceptions, self._exceptions = self._exceptions, [] - if len(exceptions) == 1: - raise exceptions[0] - elif exceptions: - raise BaseExceptionGroup( - "Multiple exceptions occurred in asynchronous callbacks", exceptions - ) - - async def _run_tests_and_fixtures( - self, - receive_stream: MemoryObjectReceiveStream[ - tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] - ], - ) -> None: - from _pytest.outcomes import OutcomeException - - with receive_stream, self._send_stream: - async for coro, future in receive_stream: - try: - retval = await coro - except CancelledError as exc: - if not future.cancelled(): - future.cancel(*exc.args) - - raise - except BaseException as exc: - if not future.cancelled(): - future.set_exception(exc) - - if not isinstance(exc, (Exception, OutcomeException)): - raise - else: - if not future.cancelled(): - future.set_result(retval) - - async def _call_in_runner_task( - self, - func: Callable[P, Awaitable[T_Retval]], - *args: P.args, - **kwargs: P.kwargs, - ) -> T_Retval: - if not self._runner_task: - self._send_stream, receive_stream = create_memory_object_stream[ - tuple[Awaitable[Any], asyncio.Future] - ](1) - self._runner_task = self.get_loop().create_task( - self._run_tests_and_fixtures(receive_stream) - ) - - coro = func(*args, **kwargs) - future: asyncio.Future[T_Retval] = self.get_loop().create_future() - self._send_stream.send_nowait((coro, future)) - return await future - - def run_asyncgen_fixture( - self, - fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], - kwargs: dict[str, Any], - ) -> Iterable[T_Retval]: - asyncgen = fixture_func(**kwargs) - fixturevalue: T_Retval = self.get_loop().run_until_complete( - self._call_in_runner_task(asyncgen.asend, None) - ) - self._raise_async_exceptions() - - yield fixturevalue - - try: - self.get_loop().run_until_complete( - self._call_in_runner_task(asyncgen.asend, None) - ) - except StopAsyncIteration: - self._raise_async_exceptions() - else: - self.get_loop().run_until_complete(asyncgen.aclose()) - raise RuntimeError("Async generator fixture did not stop") - - def run_fixture( - self, - fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], - kwargs: dict[str, Any], - ) -> T_Retval: - retval = self.get_loop().run_until_complete( - self._call_in_runner_task(fixture_func, **kwargs) - ) - self._raise_async_exceptions() - return retval - - def run_test( - self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] - ) -> None: - try: - self.get_loop().run_until_complete( - self._call_in_runner_task(test_func, **kwargs) - ) - except Exception as exc: - self._exceptions.append(exc) - - self._raise_async_exceptions() - - -class AsyncIOBackend(AsyncBackend): - @classmethod - def run( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - options: dict[str, Any], - ) -> T_Retval: - @wraps(func) - async def wrapper() -> T_Retval: - task = cast(asyncio.Task, current_task()) - task.set_name(get_callable_name(func)) - _task_states[task] = TaskState(None, None) - - try: - return await func(*args) - finally: - del _task_states[task] - - debug = options.get("debug", None) - loop_factory = options.get("loop_factory", None) - if loop_factory is None and options.get("use_uvloop", False): - import uvloop - - loop_factory = uvloop.new_event_loop - - with Runner(debug=debug, loop_factory=loop_factory) as runner: - return runner.run(wrapper()) - - @classmethod - def current_token(cls) -> object: - return get_running_loop() - - @classmethod - def current_time(cls) -> float: - return get_running_loop().time() - - @classmethod - def cancelled_exception_class(cls) -> type[BaseException]: - return CancelledError - - @classmethod - async def checkpoint(cls) -> None: - await sleep(0) - - @classmethod - async def checkpoint_if_cancelled(cls) -> None: - task = current_task() - if task is None: - return - - try: - cancel_scope = _task_states[task].cancel_scope - except KeyError: - return - - while cancel_scope: - if cancel_scope.cancel_called: - await sleep(0) - elif cancel_scope.shield: - break - else: - cancel_scope = cancel_scope._parent_scope - - @classmethod - async def cancel_shielded_checkpoint(cls) -> None: - with CancelScope(shield=True): - await sleep(0) - - @classmethod - async def sleep(cls, delay: float) -> None: - await sleep(delay) - - @classmethod - def create_cancel_scope( - cls, *, deadline: float = math.inf, shield: bool = False - ) -> CancelScope: - return CancelScope(deadline=deadline, shield=shield) - - @classmethod - def current_effective_deadline(cls) -> float: - if (task := current_task()) is None: - return math.inf - - try: - cancel_scope = _task_states[task].cancel_scope - except KeyError: - return math.inf - - deadline = math.inf - while cancel_scope: - deadline = min(deadline, cancel_scope.deadline) - if cancel_scope._cancel_called: - deadline = -math.inf - break - elif cancel_scope.shield: - break - else: - cancel_scope = cancel_scope._parent_scope - - return deadline - - @classmethod - def create_task_group(cls) -> abc.TaskGroup: - return TaskGroup() - - @classmethod - def create_event(cls) -> abc.Event: - return Event() - - @classmethod - def create_lock(cls, *, fast_acquire: bool) -> abc.Lock: - return Lock(fast_acquire=fast_acquire) - - @classmethod - def create_semaphore( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> abc.Semaphore: - return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) - - @classmethod - def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter: - return CapacityLimiter(total_tokens) - - @classmethod - async def run_sync_in_worker_thread( # type: ignore[return] - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - abandon_on_cancel: bool = False, - limiter: abc.CapacityLimiter | None = None, - ) -> T_Retval: - await cls.checkpoint() - - # If this is the first run in this event loop thread, set up the necessary - # variables - try: - idle_workers = _threadpool_idle_workers.get() - workers = _threadpool_workers.get() - except LookupError: - idle_workers = deque() - workers = set() - _threadpool_idle_workers.set(idle_workers) - _threadpool_workers.set(workers) - - async with limiter or cls.current_default_thread_limiter(): - with CancelScope(shield=not abandon_on_cancel) as scope: - future = asyncio.Future[T_Retval]() - root_task = find_root_task() - if not idle_workers: - worker = WorkerThread(root_task, workers, idle_workers) - worker.start() - workers.add(worker) - root_task.add_done_callback( - worker.stop, context=contextvars.Context() - ) - else: - worker = idle_workers.pop() - - # Prune any other workers that have been idle for MAX_IDLE_TIME - # seconds or longer - now = cls.current_time() - while idle_workers: - if ( - now - idle_workers[0].idle_since - < WorkerThread.MAX_IDLE_TIME - ): - break - - expired_worker = idle_workers.popleft() - expired_worker.root_task.remove_done_callback( - expired_worker.stop - ) - expired_worker.stop() - - context = copy_context() - context.run(sniffio.current_async_library_cvar.set, None) - if abandon_on_cancel or scope._parent_scope is None: - worker_scope = scope - else: - worker_scope = scope._parent_scope - - worker.queue.put_nowait((context, func, args, future, worker_scope)) - return await future - - @classmethod - def check_cancelled(cls) -> None: - scope: CancelScope | None = threadlocals.current_cancel_scope - while scope is not None: - if scope.cancel_called: - raise CancelledError(f"Cancelled by cancel scope {id(scope):x}") - - if scope.shield: - return - - scope = scope._parent_scope - - @classmethod - def run_async_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - async def task_wrapper(scope: CancelScope) -> T_Retval: - __tracebackhide__ = True - task = cast(asyncio.Task, current_task()) - _task_states[task] = TaskState(None, scope) - scope._tasks.add(task) - try: - return await func(*args) - except CancelledError as exc: - raise concurrent.futures.CancelledError(str(exc)) from None - finally: - scope._tasks.discard(task) - - loop = cast(AbstractEventLoop, token) - context = copy_context() - context.run(sniffio.current_async_library_cvar.set, "asyncio") - wrapper = task_wrapper(threadlocals.current_cancel_scope) - f: concurrent.futures.Future[T_Retval] = context.run( - asyncio.run_coroutine_threadsafe, wrapper, loop - ) - return f.result() - - @classmethod - def run_sync_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - @wraps(func) - def wrapper() -> None: - try: - sniffio.current_async_library_cvar.set("asyncio") - f.set_result(func(*args)) - except BaseException as exc: - f.set_exception(exc) - if not isinstance(exc, Exception): - raise - - f: concurrent.futures.Future[T_Retval] = Future() - loop = cast(AbstractEventLoop, token) - loop.call_soon_threadsafe(wrapper) - return f.result() - - @classmethod - def create_blocking_portal(cls) -> abc.BlockingPortal: - return BlockingPortal() - - @classmethod - async def open_process( - cls, - command: StrOrBytesPath | Sequence[StrOrBytesPath], - *, - stdin: int | IO[Any] | None, - stdout: int | IO[Any] | None, - stderr: int | IO[Any] | None, - **kwargs: Any, - ) -> Process: - await cls.checkpoint() - if isinstance(command, PathLike): - command = os.fspath(command) - - if isinstance(command, (str, bytes)): - process = await asyncio.create_subprocess_shell( - command, - stdin=stdin, - stdout=stdout, - stderr=stderr, - **kwargs, - ) - else: - process = await asyncio.create_subprocess_exec( - *command, - stdin=stdin, - stdout=stdout, - stderr=stderr, - **kwargs, - ) - - stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None - stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None - stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None - return Process(process, stdin_stream, stdout_stream, stderr_stream) - - @classmethod - def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: - create_task( - _shutdown_process_pool_on_exit(workers), - name="AnyIO process pool shutdown task", - ) - find_root_task().add_done_callback( - partial(_forcibly_shutdown_process_pool_on_exit, workers) # type:ignore[arg-type] - ) - - @classmethod - async def connect_tcp( - cls, host: str, port: int, local_address: IPSockAddrType | None = None - ) -> abc.SocketStream: - transport, protocol = cast( - tuple[asyncio.Transport, StreamProtocol], - await get_running_loop().create_connection( - StreamProtocol, host, port, local_addr=local_address - ), - ) - transport.pause_reading() - return SocketStream(transport, protocol) - - @classmethod - async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: - await cls.checkpoint() - loop = get_running_loop() - raw_socket = socket.socket(socket.AF_UNIX) - raw_socket.setblocking(False) - while True: - try: - raw_socket.connect(path) - except BlockingIOError: - f: asyncio.Future = asyncio.Future() - loop.add_writer(raw_socket, f.set_result, None) - f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) - await f - except BaseException: - raw_socket.close() - raise - else: - return UNIXSocketStream(raw_socket) - - @classmethod - def create_tcp_listener(cls, sock: socket.socket) -> SocketListener: - return TCPSocketListener(sock) - - @classmethod - def create_unix_listener(cls, sock: socket.socket) -> SocketListener: - return UNIXSocketListener(sock) - - @classmethod - async def create_udp_socket( - cls, - family: AddressFamily, - local_address: IPSockAddrType | None, - remote_address: IPSockAddrType | None, - reuse_port: bool, - ) -> UDPSocket | ConnectedUDPSocket: - transport, protocol = await get_running_loop().create_datagram_endpoint( - DatagramProtocol, - local_addr=local_address, - remote_addr=remote_address, - family=family, - reuse_port=reuse_port, - ) - if protocol.exception: - transport.close() - raise protocol.exception - - if not remote_address: - return UDPSocket(transport, protocol) - else: - return ConnectedUDPSocket(transport, protocol) - - @classmethod - async def create_unix_datagram_socket( # type: ignore[override] - cls, raw_socket: socket.socket, remote_path: str | bytes | None - ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: - await cls.checkpoint() - loop = get_running_loop() - - if remote_path: - while True: - try: - raw_socket.connect(remote_path) - except BlockingIOError: - f: asyncio.Future = asyncio.Future() - loop.add_writer(raw_socket, f.set_result, None) - f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) - await f - except BaseException: - raw_socket.close() - raise - else: - return ConnectedUNIXDatagramSocket(raw_socket) - else: - return UNIXDatagramSocket(raw_socket) - - @classmethod - async def getaddrinfo( - cls, - host: bytes | str | None, - port: str | int | None, - *, - family: int | AddressFamily = 0, - type: int | SocketKind = 0, - proto: int = 0, - flags: int = 0, - ) -> Sequence[ - tuple[ - AddressFamily, - SocketKind, - int, - str, - tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], - ] - ]: - return await get_running_loop().getaddrinfo( - host, port, family=family, type=type, proto=proto, flags=flags - ) - - @classmethod - async def getnameinfo( - cls, sockaddr: IPSockAddrType, flags: int = 0 - ) -> tuple[str, str]: - return await get_running_loop().getnameinfo(sockaddr, flags) - - @classmethod - async def wait_readable(cls, obj: FileDescriptorLike) -> None: - await cls.checkpoint() - try: - read_events = _read_events.get() - except LookupError: - read_events = {} - _read_events.set(read_events) - - if not isinstance(obj, int): - obj = obj.fileno() - - if read_events.get(obj): - raise BusyResourceError("reading from") - - loop = get_running_loop() - event = asyncio.Event() - try: - loop.add_reader(obj, event.set) - except NotImplementedError: - from anyio._core._asyncio_selector_thread import get_selector - - selector = get_selector() - selector.add_reader(obj, event.set) - remove_reader = selector.remove_reader - else: - remove_reader = loop.remove_reader - - read_events[obj] = event - try: - await event.wait() - finally: - remove_reader(obj) - del read_events[obj] - - @classmethod - async def wait_writable(cls, obj: FileDescriptorLike) -> None: - await cls.checkpoint() - try: - write_events = _write_events.get() - except LookupError: - write_events = {} - _write_events.set(write_events) - - if not isinstance(obj, int): - obj = obj.fileno() - - if write_events.get(obj): - raise BusyResourceError("writing to") - - loop = get_running_loop() - event = asyncio.Event() - try: - loop.add_writer(obj, event.set) - except NotImplementedError: - from anyio._core._asyncio_selector_thread import get_selector - - selector = get_selector() - selector.add_writer(obj, event.set) - remove_writer = selector.remove_writer - else: - remove_writer = loop.remove_writer - - write_events[obj] = event - try: - await event.wait() - finally: - del write_events[obj] - remove_writer(obj) - - @classmethod - def current_default_thread_limiter(cls) -> CapacityLimiter: - try: - return _default_thread_limiter.get() - except LookupError: - limiter = CapacityLimiter(40) - _default_thread_limiter.set(limiter) - return limiter - - @classmethod - def open_signal_receiver( - cls, *signals: Signals - ) -> AbstractContextManager[AsyncIterator[Signals]]: - return _SignalReceiver(signals) - - @classmethod - def get_current_task(cls) -> TaskInfo: - return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type] - - @classmethod - def get_running_tasks(cls) -> Sequence[TaskInfo]: - return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()] - - @classmethod - async def wait_all_tasks_blocked(cls) -> None: - await cls.checkpoint() - this_task = current_task() - while True: - for task in all_tasks(): - if task is this_task: - continue - - waiter = task._fut_waiter # type: ignore[attr-defined] - if waiter is None or waiter.done(): - await sleep(0.1) - break - else: - return - - @classmethod - def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: - return TestRunner(**options) - - -backend_class = AsyncIOBackend diff --git a/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py b/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py deleted file mode 100644 index b80cc04..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_backends/_trio.py +++ /dev/null @@ -1,1334 +0,0 @@ -from __future__ import annotations - -import array -import math -import os -import socket -import sys -import types -import weakref -from collections.abc import ( - AsyncGenerator, - AsyncIterator, - Awaitable, - Callable, - Collection, - Coroutine, - Iterable, - Sequence, -) -from concurrent.futures import Future -from contextlib import AbstractContextManager -from dataclasses import dataclass -from functools import partial -from io import IOBase -from os import PathLike -from signal import Signals -from socket import AddressFamily, SocketKind -from types import TracebackType -from typing import ( - IO, - TYPE_CHECKING, - Any, - Generic, - NoReturn, - TypeVar, - cast, - overload, -) - -import trio.from_thread -import trio.lowlevel -from outcome import Error, Outcome, Value -from trio.lowlevel import ( - current_root_task, - current_task, - wait_readable, - wait_writable, -) -from trio.socket import SocketType as TrioSocketType -from trio.to_thread import run_sync - -from .. import ( - CapacityLimiterStatistics, - EventStatistics, - LockStatistics, - TaskInfo, - WouldBlock, - abc, -) -from .._core._eventloop import claim_worker_thread -from .._core._exceptions import ( - BrokenResourceError, - BusyResourceError, - ClosedResourceError, - EndOfStream, -) -from .._core._sockets import convert_ipv6_sockaddr -from .._core._streams import create_memory_object_stream -from .._core._synchronization import ( - CapacityLimiter as BaseCapacityLimiter, -) -from .._core._synchronization import Event as BaseEvent -from .._core._synchronization import Lock as BaseLock -from .._core._synchronization import ( - ResourceGuard, - SemaphoreStatistics, -) -from .._core._synchronization import Semaphore as BaseSemaphore -from .._core._tasks import CancelScope as BaseCancelScope -from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType -from ..abc._eventloop import AsyncBackend, StrOrBytesPath -from ..streams.memory import MemoryObjectSendStream - -if TYPE_CHECKING: - from _typeshed import HasFileno - -if sys.version_info >= (3, 10): - from typing import ParamSpec -else: - from typing_extensions import ParamSpec - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from exceptiongroup import BaseExceptionGroup - from typing_extensions import TypeVarTuple, Unpack - -T = TypeVar("T") -T_Retval = TypeVar("T_Retval") -T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) -PosArgsT = TypeVarTuple("PosArgsT") -P = ParamSpec("P") - - -# -# Event loop -# - -RunVar = trio.lowlevel.RunVar - - -# -# Timeouts and cancellation -# - - -class CancelScope(BaseCancelScope): - def __new__( - cls, original: trio.CancelScope | None = None, **kwargs: object - ) -> CancelScope: - return object.__new__(cls) - - def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None: - self.__original = original or trio.CancelScope(**kwargs) - - def __enter__(self) -> CancelScope: - self.__original.__enter__() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool: - return self.__original.__exit__(exc_type, exc_val, exc_tb) - - def cancel(self) -> None: - self.__original.cancel() - - @property - def deadline(self) -> float: - return self.__original.deadline - - @deadline.setter - def deadline(self, value: float) -> None: - self.__original.deadline = value - - @property - def cancel_called(self) -> bool: - return self.__original.cancel_called - - @property - def cancelled_caught(self) -> bool: - return self.__original.cancelled_caught - - @property - def shield(self) -> bool: - return self.__original.shield - - @shield.setter - def shield(self, value: bool) -> None: - self.__original.shield = value - - -# -# Task groups -# - - -class TaskGroup(abc.TaskGroup): - def __init__(self) -> None: - self._active = False - self._nursery_manager = trio.open_nursery(strict_exception_groups=True) - self.cancel_scope = None # type: ignore[assignment] - - async def __aenter__(self) -> TaskGroup: - self._active = True - self._nursery = await self._nursery_manager.__aenter__() - self.cancel_scope = CancelScope(self._nursery.cancel_scope) - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool: - try: - # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type - return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value] - except BaseExceptionGroup as exc: - if not exc.split(trio.Cancelled)[1]: - raise trio.Cancelled._create() from exc - - raise - finally: - del exc_val, exc_tb - self._active = False - - def start_soon( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], - *args: Unpack[PosArgsT], - name: object = None, - ) -> None: - if not self._active: - raise RuntimeError( - "This task group is not active; no new tasks can be started." - ) - - self._nursery.start_soon(func, *args, name=name) - - async def start( - self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None - ) -> Any: - if not self._active: - raise RuntimeError( - "This task group is not active; no new tasks can be started." - ) - - return await self._nursery.start(func, *args, name=name) - - -# -# Threads -# - - -class BlockingPortal(abc.BlockingPortal): - def __new__(cls) -> BlockingPortal: - return object.__new__(cls) - - def __init__(self) -> None: - super().__init__() - self._token = trio.lowlevel.current_trio_token() - - def _spawn_task_from_thread( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - name: object, - future: Future[T_Retval], - ) -> None: - trio.from_thread.run_sync( - partial(self._task_group.start_soon, name=name), - self._call_func, - func, - args, - kwargs, - future, - trio_token=self._token, - ) - - -# -# Subprocesses -# - - -@dataclass(eq=False) -class ReceiveStreamWrapper(abc.ByteReceiveStream): - _stream: trio.abc.ReceiveStream - - async def receive(self, max_bytes: int | None = None) -> bytes: - try: - data = await self._stream.receive_some(max_bytes) - except trio.ClosedResourceError as exc: - raise ClosedResourceError from exc.__cause__ - except trio.BrokenResourceError as exc: - raise BrokenResourceError from exc.__cause__ - - if data: - return data - else: - raise EndOfStream - - async def aclose(self) -> None: - await self._stream.aclose() - - -@dataclass(eq=False) -class SendStreamWrapper(abc.ByteSendStream): - _stream: trio.abc.SendStream - - async def send(self, item: bytes) -> None: - try: - await self._stream.send_all(item) - except trio.ClosedResourceError as exc: - raise ClosedResourceError from exc.__cause__ - except trio.BrokenResourceError as exc: - raise BrokenResourceError from exc.__cause__ - - async def aclose(self) -> None: - await self._stream.aclose() - - -@dataclass(eq=False) -class Process(abc.Process): - _process: trio.Process - _stdin: abc.ByteSendStream | None - _stdout: abc.ByteReceiveStream | None - _stderr: abc.ByteReceiveStream | None - - async def aclose(self) -> None: - with CancelScope(shield=True): - if self._stdin: - await self._stdin.aclose() - if self._stdout: - await self._stdout.aclose() - if self._stderr: - await self._stderr.aclose() - - try: - await self.wait() - except BaseException: - self.kill() - with CancelScope(shield=True): - await self.wait() - raise - - async def wait(self) -> int: - return await self._process.wait() - - def terminate(self) -> None: - self._process.terminate() - - def kill(self) -> None: - self._process.kill() - - def send_signal(self, signal: Signals) -> None: - self._process.send_signal(signal) - - @property - def pid(self) -> int: - return self._process.pid - - @property - def returncode(self) -> int | None: - return self._process.returncode - - @property - def stdin(self) -> abc.ByteSendStream | None: - return self._stdin - - @property - def stdout(self) -> abc.ByteReceiveStream | None: - return self._stdout - - @property - def stderr(self) -> abc.ByteReceiveStream | None: - return self._stderr - - -class _ProcessPoolShutdownInstrument(trio.abc.Instrument): - def after_run(self) -> None: - super().after_run() - - -current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar( - "current_default_worker_process_limiter" -) - - -async def _shutdown_process_pool(workers: set[abc.Process]) -> None: - try: - await trio.sleep(math.inf) - except trio.Cancelled: - for process in workers: - if process.returncode is None: - process.kill() - - with CancelScope(shield=True): - for process in workers: - await process.aclose() - - -# -# Sockets and networking -# - - -class _TrioSocketMixin(Generic[T_SockAddr]): - def __init__(self, trio_socket: TrioSocketType) -> None: - self._trio_socket = trio_socket - self._closed = False - - def _check_closed(self) -> None: - if self._closed: - raise ClosedResourceError - if self._trio_socket.fileno() < 0: - raise BrokenResourceError - - @property - def _raw_socket(self) -> socket.socket: - return self._trio_socket._sock # type: ignore[attr-defined] - - async def aclose(self) -> None: - if self._trio_socket.fileno() >= 0: - self._closed = True - self._trio_socket.close() - - def _convert_socket_error(self, exc: BaseException) -> NoReturn: - if isinstance(exc, trio.ClosedResourceError): - raise ClosedResourceError from exc - elif self._trio_socket.fileno() < 0 and self._closed: - raise ClosedResourceError from None - elif isinstance(exc, OSError): - raise BrokenResourceError from exc - else: - raise exc - - -class SocketStream(_TrioSocketMixin, abc.SocketStream): - def __init__(self, trio_socket: TrioSocketType) -> None: - super().__init__(trio_socket) - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - async def receive(self, max_bytes: int = 65536) -> bytes: - with self._receive_guard: - try: - data = await self._trio_socket.recv(max_bytes) - except BaseException as exc: - self._convert_socket_error(exc) - - if data: - return data - else: - raise EndOfStream - - async def send(self, item: bytes) -> None: - with self._send_guard: - view = memoryview(item) - while view: - try: - bytes_sent = await self._trio_socket.send(view) - except BaseException as exc: - self._convert_socket_error(exc) - - view = view[bytes_sent:] - - async def send_eof(self) -> None: - self._trio_socket.shutdown(socket.SHUT_WR) - - -class UNIXSocketStream(SocketStream, abc.UNIXSocketStream): - async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: - if not isinstance(msglen, int) or msglen < 0: - raise ValueError("msglen must be a non-negative integer") - if not isinstance(maxfds, int) or maxfds < 1: - raise ValueError("maxfds must be a positive integer") - - fds = array.array("i") - await trio.lowlevel.checkpoint() - with self._receive_guard: - while True: - try: - message, ancdata, flags, addr = await self._trio_socket.recvmsg( - msglen, socket.CMSG_LEN(maxfds * fds.itemsize) - ) - except BaseException as exc: - self._convert_socket_error(exc) - else: - if not message and not ancdata: - raise EndOfStream - - break - - for cmsg_level, cmsg_type, cmsg_data in ancdata: - if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: - raise RuntimeError( - f"Received unexpected ancillary data; message = {message!r}, " - f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" - ) - - fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) - - return message, list(fds) - - async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: - if not message: - raise ValueError("message must not be empty") - if not fds: - raise ValueError("fds must not be empty") - - filenos: list[int] = [] - for fd in fds: - if isinstance(fd, int): - filenos.append(fd) - elif isinstance(fd, IOBase): - filenos.append(fd.fileno()) - - fdarray = array.array("i", filenos) - await trio.lowlevel.checkpoint() - with self._send_guard: - while True: - try: - await self._trio_socket.sendmsg( - [message], - [ - ( - socket.SOL_SOCKET, - socket.SCM_RIGHTS, - fdarray, - ) - ], - ) - break - except BaseException as exc: - self._convert_socket_error(exc) - - -class TCPSocketListener(_TrioSocketMixin, abc.SocketListener): - def __init__(self, raw_socket: socket.socket): - super().__init__(trio.socket.from_stdlib_socket(raw_socket)) - self._accept_guard = ResourceGuard("accepting connections from") - - async def accept(self) -> SocketStream: - with self._accept_guard: - try: - trio_socket, _addr = await self._trio_socket.accept() - except BaseException as exc: - self._convert_socket_error(exc) - - trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - return SocketStream(trio_socket) - - -class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener): - def __init__(self, raw_socket: socket.socket): - super().__init__(trio.socket.from_stdlib_socket(raw_socket)) - self._accept_guard = ResourceGuard("accepting connections from") - - async def accept(self) -> UNIXSocketStream: - with self._accept_guard: - try: - trio_socket, _addr = await self._trio_socket.accept() - except BaseException as exc: - self._convert_socket_error(exc) - - return UNIXSocketStream(trio_socket) - - -class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket): - def __init__(self, trio_socket: TrioSocketType) -> None: - super().__init__(trio_socket) - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - async def receive(self) -> tuple[bytes, IPSockAddrType]: - with self._receive_guard: - try: - data, addr = await self._trio_socket.recvfrom(65536) - return data, convert_ipv6_sockaddr(addr) - except BaseException as exc: - self._convert_socket_error(exc) - - async def send(self, item: UDPPacketType) -> None: - with self._send_guard: - try: - await self._trio_socket.sendto(*item) - except BaseException as exc: - self._convert_socket_error(exc) - - -class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket): - def __init__(self, trio_socket: TrioSocketType) -> None: - super().__init__(trio_socket) - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - async def receive(self) -> bytes: - with self._receive_guard: - try: - return await self._trio_socket.recv(65536) - except BaseException as exc: - self._convert_socket_error(exc) - - async def send(self, item: bytes) -> None: - with self._send_guard: - try: - await self._trio_socket.send(item) - except BaseException as exc: - self._convert_socket_error(exc) - - -class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket): - def __init__(self, trio_socket: TrioSocketType) -> None: - super().__init__(trio_socket) - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - async def receive(self) -> UNIXDatagramPacketType: - with self._receive_guard: - try: - data, addr = await self._trio_socket.recvfrom(65536) - return data, addr - except BaseException as exc: - self._convert_socket_error(exc) - - async def send(self, item: UNIXDatagramPacketType) -> None: - with self._send_guard: - try: - await self._trio_socket.sendto(*item) - except BaseException as exc: - self._convert_socket_error(exc) - - -class ConnectedUNIXDatagramSocket( - _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket -): - def __init__(self, trio_socket: TrioSocketType) -> None: - super().__init__(trio_socket) - self._receive_guard = ResourceGuard("reading from") - self._send_guard = ResourceGuard("writing to") - - async def receive(self) -> bytes: - with self._receive_guard: - try: - return await self._trio_socket.recv(65536) - except BaseException as exc: - self._convert_socket_error(exc) - - async def send(self, item: bytes) -> None: - with self._send_guard: - try: - await self._trio_socket.send(item) - except BaseException as exc: - self._convert_socket_error(exc) - - -# -# Synchronization -# - - -class Event(BaseEvent): - def __new__(cls) -> Event: - return object.__new__(cls) - - def __init__(self) -> None: - self.__original = trio.Event() - - def is_set(self) -> bool: - return self.__original.is_set() - - async def wait(self) -> None: - return await self.__original.wait() - - def statistics(self) -> EventStatistics: - orig_statistics = self.__original.statistics() - return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting) - - def set(self) -> None: - self.__original.set() - - -class Lock(BaseLock): - def __new__(cls, *, fast_acquire: bool = False) -> Lock: - return object.__new__(cls) - - def __init__(self, *, fast_acquire: bool = False) -> None: - self._fast_acquire = fast_acquire - self.__original = trio.Lock() - - @staticmethod - def _convert_runtime_error_msg(exc: RuntimeError) -> None: - if exc.args == ("attempt to re-acquire an already held Lock",): - exc.args = ("Attempted to acquire an already held Lock",) - - async def acquire(self) -> None: - if not self._fast_acquire: - try: - await self.__original.acquire() - except RuntimeError as exc: - self._convert_runtime_error_msg(exc) - raise - - return - - # This is the "fast path" where we don't let other tasks run - await trio.lowlevel.checkpoint_if_cancelled() - try: - self.__original.acquire_nowait() - except trio.WouldBlock: - await self.__original._lot.park() - except RuntimeError as exc: - self._convert_runtime_error_msg(exc) - raise - - def acquire_nowait(self) -> None: - try: - self.__original.acquire_nowait() - except trio.WouldBlock: - raise WouldBlock from None - except RuntimeError as exc: - self._convert_runtime_error_msg(exc) - raise - - def locked(self) -> bool: - return self.__original.locked() - - def release(self) -> None: - self.__original.release() - - def statistics(self) -> LockStatistics: - orig_statistics = self.__original.statistics() - owner = TrioTaskInfo(orig_statistics.owner) if orig_statistics.owner else None - return LockStatistics( - orig_statistics.locked, owner, orig_statistics.tasks_waiting - ) - - -class Semaphore(BaseSemaphore): - def __new__( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> Semaphore: - return object.__new__(cls) - - def __init__( - self, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> None: - super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) - self.__original = trio.Semaphore(initial_value, max_value=max_value) - - async def acquire(self) -> None: - if not self._fast_acquire: - await self.__original.acquire() - return - - # This is the "fast path" where we don't let other tasks run - await trio.lowlevel.checkpoint_if_cancelled() - try: - self.__original.acquire_nowait() - except trio.WouldBlock: - await self.__original._lot.park() - - def acquire_nowait(self) -> None: - try: - self.__original.acquire_nowait() - except trio.WouldBlock: - raise WouldBlock from None - - @property - def max_value(self) -> int | None: - return self.__original.max_value - - @property - def value(self) -> int: - return self.__original.value - - def release(self) -> None: - self.__original.release() - - def statistics(self) -> SemaphoreStatistics: - orig_statistics = self.__original.statistics() - return SemaphoreStatistics(orig_statistics.tasks_waiting) - - -class CapacityLimiter(BaseCapacityLimiter): - def __new__( - cls, - total_tokens: float | None = None, - *, - original: trio.CapacityLimiter | None = None, - ) -> CapacityLimiter: - return object.__new__(cls) - - def __init__( - self, - total_tokens: float | None = None, - *, - original: trio.CapacityLimiter | None = None, - ) -> None: - if original is not None: - self.__original = original - else: - assert total_tokens is not None - self.__original = trio.CapacityLimiter(total_tokens) - - async def __aenter__(self) -> None: - return await self.__original.__aenter__() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.__original.__aexit__(exc_type, exc_val, exc_tb) - - @property - def total_tokens(self) -> float: - return self.__original.total_tokens - - @total_tokens.setter - def total_tokens(self, value: float) -> None: - self.__original.total_tokens = value - - @property - def borrowed_tokens(self) -> int: - return self.__original.borrowed_tokens - - @property - def available_tokens(self) -> float: - return self.__original.available_tokens - - def acquire_nowait(self) -> None: - self.__original.acquire_nowait() - - def acquire_on_behalf_of_nowait(self, borrower: object) -> None: - self.__original.acquire_on_behalf_of_nowait(borrower) - - async def acquire(self) -> None: - await self.__original.acquire() - - async def acquire_on_behalf_of(self, borrower: object) -> None: - await self.__original.acquire_on_behalf_of(borrower) - - def release(self) -> None: - return self.__original.release() - - def release_on_behalf_of(self, borrower: object) -> None: - return self.__original.release_on_behalf_of(borrower) - - def statistics(self) -> CapacityLimiterStatistics: - orig = self.__original.statistics() - return CapacityLimiterStatistics( - borrowed_tokens=orig.borrowed_tokens, - total_tokens=orig.total_tokens, - borrowers=tuple(orig.borrowers), - tasks_waiting=orig.tasks_waiting, - ) - - -_capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper") - - -# -# Signal handling -# - - -class _SignalReceiver: - _iterator: AsyncIterator[int] - - def __init__(self, signals: tuple[Signals, ...]): - self._signals = signals - - def __enter__(self) -> _SignalReceiver: - self._cm = trio.open_signal_receiver(*self._signals) - self._iterator = self._cm.__enter__() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - return self._cm.__exit__(exc_type, exc_val, exc_tb) - - def __aiter__(self) -> _SignalReceiver: - return self - - async def __anext__(self) -> Signals: - signum = await self._iterator.__anext__() - return Signals(signum) - - -# -# Testing and debugging -# - - -class TestRunner(abc.TestRunner): - def __init__(self, **options: Any) -> None: - from queue import Queue - - self._call_queue: Queue[Callable[[], object]] = Queue() - self._send_stream: MemoryObjectSendStream | None = None - self._options = options - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> None: - if self._send_stream: - self._send_stream.close() - while self._send_stream is not None: - self._call_queue.get()() - - async def _run_tests_and_fixtures(self) -> None: - self._send_stream, receive_stream = create_memory_object_stream(1) - with receive_stream: - async for coro, outcome_holder in receive_stream: - try: - retval = await coro - except BaseException as exc: - outcome_holder.append(Error(exc)) - else: - outcome_holder.append(Value(retval)) - - def _main_task_finished(self, outcome: object) -> None: - self._send_stream = None - - def _call_in_runner_task( - self, - func: Callable[P, Awaitable[T_Retval]], - *args: P.args, - **kwargs: P.kwargs, - ) -> T_Retval: - if self._send_stream is None: - trio.lowlevel.start_guest_run( - self._run_tests_and_fixtures, - run_sync_soon_threadsafe=self._call_queue.put, - done_callback=self._main_task_finished, - **self._options, - ) - while self._send_stream is None: - self._call_queue.get()() - - outcome_holder: list[Outcome] = [] - self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder)) - while not outcome_holder: - self._call_queue.get()() - - return outcome_holder[0].unwrap() - - def run_asyncgen_fixture( - self, - fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], - kwargs: dict[str, Any], - ) -> Iterable[T_Retval]: - asyncgen = fixture_func(**kwargs) - fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None) - - yield fixturevalue - - try: - self._call_in_runner_task(asyncgen.asend, None) - except StopAsyncIteration: - pass - else: - self._call_in_runner_task(asyncgen.aclose) - raise RuntimeError("Async generator fixture did not stop") - - def run_fixture( - self, - fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], - kwargs: dict[str, Any], - ) -> T_Retval: - return self._call_in_runner_task(fixture_func, **kwargs) - - def run_test( - self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] - ) -> None: - self._call_in_runner_task(test_func, **kwargs) - - -class TrioTaskInfo(TaskInfo): - def __init__(self, task: trio.lowlevel.Task): - parent_id = None - if task.parent_nursery and task.parent_nursery.parent_task: - parent_id = id(task.parent_nursery.parent_task) - - super().__init__(id(task), parent_id, task.name, task.coro) - self._task = weakref.proxy(task) - - def has_pending_cancellation(self) -> bool: - try: - return self._task._cancel_status.effectively_cancelled - except ReferenceError: - # If the task is no longer around, it surely doesn't have a cancellation - # pending - return False - - -class TrioBackend(AsyncBackend): - @classmethod - def run( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - options: dict[str, Any], - ) -> T_Retval: - return trio.run(func, *args) - - @classmethod - def current_token(cls) -> object: - return trio.lowlevel.current_trio_token() - - @classmethod - def current_time(cls) -> float: - return trio.current_time() - - @classmethod - def cancelled_exception_class(cls) -> type[BaseException]: - return trio.Cancelled - - @classmethod - async def checkpoint(cls) -> None: - await trio.lowlevel.checkpoint() - - @classmethod - async def checkpoint_if_cancelled(cls) -> None: - await trio.lowlevel.checkpoint_if_cancelled() - - @classmethod - async def cancel_shielded_checkpoint(cls) -> None: - await trio.lowlevel.cancel_shielded_checkpoint() - - @classmethod - async def sleep(cls, delay: float) -> None: - await trio.sleep(delay) - - @classmethod - def create_cancel_scope( - cls, *, deadline: float = math.inf, shield: bool = False - ) -> abc.CancelScope: - return CancelScope(deadline=deadline, shield=shield) - - @classmethod - def current_effective_deadline(cls) -> float: - return trio.current_effective_deadline() - - @classmethod - def create_task_group(cls) -> abc.TaskGroup: - return TaskGroup() - - @classmethod - def create_event(cls) -> abc.Event: - return Event() - - @classmethod - def create_lock(cls, *, fast_acquire: bool) -> Lock: - return Lock(fast_acquire=fast_acquire) - - @classmethod - def create_semaphore( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> abc.Semaphore: - return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) - - @classmethod - def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: - return CapacityLimiter(total_tokens) - - @classmethod - async def run_sync_in_worker_thread( - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - abandon_on_cancel: bool = False, - limiter: abc.CapacityLimiter | None = None, - ) -> T_Retval: - def wrapper() -> T_Retval: - with claim_worker_thread(TrioBackend, token): - return func(*args) - - token = TrioBackend.current_token() - return await run_sync( - wrapper, - abandon_on_cancel=abandon_on_cancel, - limiter=cast(trio.CapacityLimiter, limiter), - ) - - @classmethod - def check_cancelled(cls) -> None: - trio.from_thread.check_cancelled() - - @classmethod - def run_async_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - return trio.from_thread.run(func, *args) - - @classmethod - def run_sync_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - return trio.from_thread.run_sync(func, *args) - - @classmethod - def create_blocking_portal(cls) -> abc.BlockingPortal: - return BlockingPortal() - - @classmethod - async def open_process( - cls, - command: StrOrBytesPath | Sequence[StrOrBytesPath], - *, - stdin: int | IO[Any] | None, - stdout: int | IO[Any] | None, - stderr: int | IO[Any] | None, - **kwargs: Any, - ) -> Process: - def convert_item(item: StrOrBytesPath) -> str: - str_or_bytes = os.fspath(item) - if isinstance(str_or_bytes, str): - return str_or_bytes - else: - return os.fsdecode(str_or_bytes) - - if isinstance(command, (str, bytes, PathLike)): - process = await trio.lowlevel.open_process( - convert_item(command), - stdin=stdin, - stdout=stdout, - stderr=stderr, - shell=True, - **kwargs, - ) - else: - process = await trio.lowlevel.open_process( - [convert_item(item) for item in command], - stdin=stdin, - stdout=stdout, - stderr=stderr, - shell=False, - **kwargs, - ) - - stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None - stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None - stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None - return Process(process, stdin_stream, stdout_stream, stderr_stream) - - @classmethod - def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: - trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers) - - @classmethod - async def connect_tcp( - cls, host: str, port: int, local_address: IPSockAddrType | None = None - ) -> SocketStream: - family = socket.AF_INET6 if ":" in host else socket.AF_INET - trio_socket = trio.socket.socket(family) - trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - if local_address: - await trio_socket.bind(local_address) - - try: - await trio_socket.connect((host, port)) - except BaseException: - trio_socket.close() - raise - - return SocketStream(trio_socket) - - @classmethod - async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: - trio_socket = trio.socket.socket(socket.AF_UNIX) - try: - await trio_socket.connect(path) - except BaseException: - trio_socket.close() - raise - - return UNIXSocketStream(trio_socket) - - @classmethod - def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener: - return TCPSocketListener(sock) - - @classmethod - def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener: - return UNIXSocketListener(sock) - - @classmethod - async def create_udp_socket( - cls, - family: socket.AddressFamily, - local_address: IPSockAddrType | None, - remote_address: IPSockAddrType | None, - reuse_port: bool, - ) -> UDPSocket | ConnectedUDPSocket: - trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM) - - if reuse_port: - trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - - if local_address: - await trio_socket.bind(local_address) - - if remote_address: - await trio_socket.connect(remote_address) - return ConnectedUDPSocket(trio_socket) - else: - return UDPSocket(trio_socket) - - @classmethod - @overload - async def create_unix_datagram_socket( - cls, raw_socket: socket.socket, remote_path: None - ) -> abc.UNIXDatagramSocket: ... - - @classmethod - @overload - async def create_unix_datagram_socket( - cls, raw_socket: socket.socket, remote_path: str | bytes - ) -> abc.ConnectedUNIXDatagramSocket: ... - - @classmethod - async def create_unix_datagram_socket( - cls, raw_socket: socket.socket, remote_path: str | bytes | None - ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: - trio_socket = trio.socket.from_stdlib_socket(raw_socket) - - if remote_path: - await trio_socket.connect(remote_path) - return ConnectedUNIXDatagramSocket(trio_socket) - else: - return UNIXDatagramSocket(trio_socket) - - @classmethod - async def getaddrinfo( - cls, - host: bytes | str | None, - port: str | int | None, - *, - family: int | AddressFamily = 0, - type: int | SocketKind = 0, - proto: int = 0, - flags: int = 0, - ) -> Sequence[ - tuple[ - AddressFamily, - SocketKind, - int, - str, - tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], - ] - ]: - return await trio.socket.getaddrinfo(host, port, family, type, proto, flags) - - @classmethod - async def getnameinfo( - cls, sockaddr: IPSockAddrType, flags: int = 0 - ) -> tuple[str, str]: - return await trio.socket.getnameinfo(sockaddr, flags) - - @classmethod - async def wait_readable(cls, obj: HasFileno | int) -> None: - try: - await wait_readable(obj) - except trio.ClosedResourceError as exc: - raise ClosedResourceError().with_traceback(exc.__traceback__) from None - except trio.BusyResourceError: - raise BusyResourceError("reading from") from None - - @classmethod - async def wait_writable(cls, obj: HasFileno | int) -> None: - try: - await wait_writable(obj) - except trio.ClosedResourceError as exc: - raise ClosedResourceError().with_traceback(exc.__traceback__) from None - except trio.BusyResourceError: - raise BusyResourceError("writing to") from None - - @classmethod - def current_default_thread_limiter(cls) -> CapacityLimiter: - try: - return _capacity_limiter_wrapper.get() - except LookupError: - limiter = CapacityLimiter( - original=trio.to_thread.current_default_thread_limiter() - ) - _capacity_limiter_wrapper.set(limiter) - return limiter - - @classmethod - def open_signal_receiver( - cls, *signals: Signals - ) -> AbstractContextManager[AsyncIterator[Signals]]: - return _SignalReceiver(signals) - - @classmethod - def get_current_task(cls) -> TaskInfo: - task = current_task() - return TrioTaskInfo(task) - - @classmethod - def get_running_tasks(cls) -> Sequence[TaskInfo]: - root_task = current_root_task() - assert root_task - task_infos = [TrioTaskInfo(root_task)] - nurseries = root_task.child_nurseries - while nurseries: - new_nurseries: list[trio.Nursery] = [] - for nursery in nurseries: - for task in nursery.child_tasks: - task_infos.append(TrioTaskInfo(task)) - new_nurseries.extend(task.child_nurseries) - - nurseries = new_nurseries - - return task_infos - - @classmethod - async def wait_all_tasks_blocked(cls) -> None: - from trio.testing import wait_all_tasks_blocked - - await wait_all_tasks_blocked() - - @classmethod - def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: - return TestRunner(**options) - - -backend_class = TrioBackend diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__init__.py b/venv/lib/python3.12/site-packages/anyio/_core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 23abd8e..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc deleted file mode 100644 index 30a1c5d..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc deleted file mode 100644 index 0c04cd3..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_eventloop.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc deleted file mode 100644 index 00498e9..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_exceptions.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc deleted file mode 100644 index 5bdad0c..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_fileio.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc deleted file mode 100644 index f9f11d0..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_resources.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc deleted file mode 100644 index b97dbcf..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_signals.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc deleted file mode 100644 index b71ce07..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_sockets.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc deleted file mode 100644 index 648beeb..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_streams.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc deleted file mode 100644 index 5047650..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc deleted file mode 100644 index ec0e3c1..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_synchronization.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc deleted file mode 100644 index dacc46b..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tasks.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc deleted file mode 100644 index 19ddfc0..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_tempfile.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc deleted file mode 100644 index 4305c10..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_testing.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc deleted file mode 100644 index 63a6130..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/_core/__pycache__/_typedattr.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py b/venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py deleted file mode 100644 index 9f35bae..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_asyncio_selector_thread.py +++ /dev/null @@ -1,167 +0,0 @@ -from __future__ import annotations - -import asyncio -import socket -import threading -from collections.abc import Callable -from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from _typeshed import FileDescriptorLike - -_selector_lock = threading.Lock() -_selector: Selector | None = None - - -class Selector: - def __init__(self) -> None: - self._thread = threading.Thread(target=self.run, name="AnyIO socket selector") - self._selector = DefaultSelector() - self._send, self._receive = socket.socketpair() - self._send.setblocking(False) - self._receive.setblocking(False) - # This somewhat reduces the amount of memory wasted queueing up data - # for wakeups. With these settings, maximum number of 1-byte sends - # before getting BlockingIOError: - # Linux 4.8: 6 - # macOS (darwin 15.5): 1 - # Windows 10: 525347 - # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send - # blocking, even on non-blocking sockets, so don't do that.) - self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1) - self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1) - # On Windows this is a TCP socket so this might matter. On other - # platforms this fails b/c AF_UNIX sockets aren't actually TCP. - try: - self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - except OSError: - pass - - self._selector.register(self._receive, EVENT_READ) - self._closed = False - - def start(self) -> None: - self._thread.start() - threading._register_atexit(self._stop) # type: ignore[attr-defined] - - def _stop(self) -> None: - global _selector - self._closed = True - self._notify_self() - self._send.close() - self._thread.join() - self._selector.unregister(self._receive) - self._receive.close() - self._selector.close() - _selector = None - assert not self._selector.get_map(), ( - "selector still has registered file descriptors after shutdown" - ) - - def _notify_self(self) -> None: - try: - self._send.send(b"\x00") - except BlockingIOError: - pass - - def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: - loop = asyncio.get_running_loop() - try: - key = self._selector.get_key(fd) - except KeyError: - self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)}) - else: - if EVENT_READ in key.data: - raise ValueError( - "this file descriptor is already registered for reading" - ) - - key.data[EVENT_READ] = loop, callback - self._selector.modify(fd, key.events | EVENT_READ, key.data) - - self._notify_self() - - def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: - loop = asyncio.get_running_loop() - try: - key = self._selector.get_key(fd) - except KeyError: - self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)}) - else: - if EVENT_WRITE in key.data: - raise ValueError( - "this file descriptor is already registered for writing" - ) - - key.data[EVENT_WRITE] = loop, callback - self._selector.modify(fd, key.events | EVENT_WRITE, key.data) - - self._notify_self() - - def remove_reader(self, fd: FileDescriptorLike) -> bool: - try: - key = self._selector.get_key(fd) - except KeyError: - return False - - if new_events := key.events ^ EVENT_READ: - del key.data[EVENT_READ] - self._selector.modify(fd, new_events, key.data) - else: - self._selector.unregister(fd) - - return True - - def remove_writer(self, fd: FileDescriptorLike) -> bool: - try: - key = self._selector.get_key(fd) - except KeyError: - return False - - if new_events := key.events ^ EVENT_WRITE: - del key.data[EVENT_WRITE] - self._selector.modify(fd, new_events, key.data) - else: - self._selector.unregister(fd) - - return True - - def run(self) -> None: - while not self._closed: - for key, events in self._selector.select(): - if key.fileobj is self._receive: - try: - while self._receive.recv(4096): - pass - except BlockingIOError: - pass - - continue - - if events & EVENT_READ: - loop, callback = key.data[EVENT_READ] - self.remove_reader(key.fd) - try: - loop.call_soon_threadsafe(callback) - except RuntimeError: - pass # the loop was already closed - - if events & EVENT_WRITE: - loop, callback = key.data[EVENT_WRITE] - self.remove_writer(key.fd) - try: - loop.call_soon_threadsafe(callback) - except RuntimeError: - pass # the loop was already closed - - -def get_selector() -> Selector: - global _selector - - with _selector_lock: - if _selector is None: - _selector = Selector() - _selector.start() - - return _selector diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py b/venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py deleted file mode 100644 index 6dcb458..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_eventloop.py +++ /dev/null @@ -1,166 +0,0 @@ -from __future__ import annotations - -import math -import sys -import threading -from collections.abc import Awaitable, Callable, Generator -from contextlib import contextmanager -from importlib import import_module -from typing import TYPE_CHECKING, Any, TypeVar - -import sniffio - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -if TYPE_CHECKING: - from ..abc import AsyncBackend - -# This must be updated when new backends are introduced -BACKENDS = "asyncio", "trio" - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") - -threadlocals = threading.local() -loaded_backends: dict[str, type[AsyncBackend]] = {} - - -def run( - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - *args: Unpack[PosArgsT], - backend: str = "asyncio", - backend_options: dict[str, Any] | None = None, -) -> T_Retval: - """ - Run the given coroutine function in an asynchronous event loop. - - The current thread must not be already running an event loop. - - :param func: a coroutine function - :param args: positional arguments to ``func`` - :param backend: name of the asynchronous event loop implementation – currently - either ``asyncio`` or ``trio`` - :param backend_options: keyword arguments to call the backend ``run()`` - implementation with (documented :ref:`here `) - :return: the return value of the coroutine function - :raises RuntimeError: if an asynchronous event loop is already running in this - thread - :raises LookupError: if the named backend is not found - - """ - try: - asynclib_name = sniffio.current_async_library() - except sniffio.AsyncLibraryNotFoundError: - pass - else: - raise RuntimeError(f"Already running {asynclib_name} in this thread") - - try: - async_backend = get_async_backend(backend) - except ImportError as exc: - raise LookupError(f"No such backend: {backend}") from exc - - token = None - if sniffio.current_async_library_cvar.get(None) is None: - # Since we're in control of the event loop, we can cache the name of the async - # library - token = sniffio.current_async_library_cvar.set(backend) - - try: - backend_options = backend_options or {} - return async_backend.run(func, args, {}, backend_options) - finally: - if token: - sniffio.current_async_library_cvar.reset(token) - - -async def sleep(delay: float) -> None: - """ - Pause the current task for the specified duration. - - :param delay: the duration, in seconds - - """ - return await get_async_backend().sleep(delay) - - -async def sleep_forever() -> None: - """ - Pause the current task until it's cancelled. - - This is a shortcut for ``sleep(math.inf)``. - - .. versionadded:: 3.1 - - """ - await sleep(math.inf) - - -async def sleep_until(deadline: float) -> None: - """ - Pause the current task until the given time. - - :param deadline: the absolute time to wake up at (according to the internal - monotonic clock of the event loop) - - .. versionadded:: 3.1 - - """ - now = current_time() - await sleep(max(deadline - now, 0)) - - -def current_time() -> float: - """ - Return the current value of the event loop's internal clock. - - :return: the clock value (seconds) - - """ - return get_async_backend().current_time() - - -def get_all_backends() -> tuple[str, ...]: - """Return a tuple of the names of all built-in backends.""" - return BACKENDS - - -def get_cancelled_exc_class() -> type[BaseException]: - """Return the current async library's cancellation exception class.""" - return get_async_backend().cancelled_exception_class() - - -# -# Private API -# - - -@contextmanager -def claim_worker_thread( - backend_class: type[AsyncBackend], token: object -) -> Generator[Any, None, None]: - threadlocals.current_async_backend = backend_class - threadlocals.current_token = token - try: - yield - finally: - del threadlocals.current_async_backend - del threadlocals.current_token - - -def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: - if asynclib_name is None: - asynclib_name = sniffio.current_async_library() - - # We use our own dict instead of sys.modules to get the already imported back-end - # class because the appropriate modules in sys.modules could potentially be only - # partially initialized - try: - return loaded_backends[asynclib_name] - except KeyError: - module = import_module(f"anyio._backends._{asynclib_name}") - loaded_backends[asynclib_name] = module.backend_class - return module.backend_class diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py b/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py deleted file mode 100644 index 16b9448..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_exceptions.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import Generator -from textwrap import dedent -from typing import Any - -if sys.version_info < (3, 11): - from exceptiongroup import BaseExceptionGroup - - -class BrokenResourceError(Exception): - """ - Raised when trying to use a resource that has been rendered unusable due to external - causes (e.g. a send stream whose peer has disconnected). - """ - - -class BrokenWorkerProcess(Exception): - """ - Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or - otherwise misbehaves. - """ - - -class BrokenWorkerIntepreter(Exception): - """ - Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is - raised in the subinterpreter. - """ - - def __init__(self, excinfo: Any): - # This was adapted from concurrent.futures.interpreter.ExecutionFailed - msg = excinfo.formatted - if not msg: - if excinfo.type and excinfo.msg: - msg = f"{excinfo.type.__name__}: {excinfo.msg}" - else: - msg = excinfo.type.__name__ or excinfo.msg - - super().__init__(msg) - self.excinfo = excinfo - - def __str__(self) -> str: - try: - formatted = self.excinfo.errdisplay - except Exception: - return super().__str__() - else: - return dedent( - f""" - {super().__str__()} - - Uncaught in the interpreter: - - {formatted} - """.strip() - ) - - -class BusyResourceError(Exception): - """ - Raised when two tasks are trying to read from or write to the same resource - concurrently. - """ - - def __init__(self, action: str): - super().__init__(f"Another task is already {action} this resource") - - -class ClosedResourceError(Exception): - """Raised when trying to use a resource that has been closed.""" - - -class DelimiterNotFound(Exception): - """ - Raised during - :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the - maximum number of bytes has been read without the delimiter being found. - """ - - def __init__(self, max_bytes: int) -> None: - super().__init__( - f"The delimiter was not found among the first {max_bytes} bytes" - ) - - -class EndOfStream(Exception): - """ - Raised when trying to read from a stream that has been closed from the other end. - """ - - -class IncompleteRead(Exception): - """ - Raised during - :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or - :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the - connection is closed before the requested amount of bytes has been read. - """ - - def __init__(self) -> None: - super().__init__( - "The stream was closed before the read operation could be completed" - ) - - -class TypedAttributeLookupError(LookupError): - """ - Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute - is not found and no default value has been given. - """ - - -class WouldBlock(Exception): - """Raised by ``X_nowait`` functions if ``X()`` would block.""" - - -def iterate_exceptions( - exception: BaseException, -) -> Generator[BaseException, None, None]: - if isinstance(exception, BaseExceptionGroup): - for exc in exception.exceptions: - yield from iterate_exceptions(exc) - else: - yield exception diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py b/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py deleted file mode 100644 index a0d6198..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_fileio.py +++ /dev/null @@ -1,742 +0,0 @@ -from __future__ import annotations - -import os -import pathlib -import sys -from collections.abc import ( - AsyncIterator, - Callable, - Iterable, - Iterator, - Sequence, -) -from dataclasses import dataclass -from functools import partial -from os import PathLike -from typing import ( - IO, - TYPE_CHECKING, - Any, - AnyStr, - ClassVar, - Final, - Generic, - overload, -) - -from .. import to_thread -from ..abc import AsyncResource - -if TYPE_CHECKING: - from types import ModuleType - - from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer -else: - ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object - - -class AsyncFile(AsyncResource, Generic[AnyStr]): - """ - An asynchronous file object. - - This class wraps a standard file object and provides async friendly versions of the - following blocking methods (where available on the original file object): - - * read - * read1 - * readline - * readlines - * readinto - * readinto1 - * write - * writelines - * truncate - * seek - * tell - * flush - - All other methods are directly passed through. - - This class supports the asynchronous context manager protocol which closes the - underlying file at the end of the context block. - - This class also supports asynchronous iteration:: - - async with await open_file(...) as f: - async for line in f: - print(line) - """ - - def __init__(self, fp: IO[AnyStr]) -> None: - self._fp: Any = fp - - def __getattr__(self, name: str) -> object: - return getattr(self._fp, name) - - @property - def wrapped(self) -> IO[AnyStr]: - """The wrapped file object.""" - return self._fp - - async def __aiter__(self) -> AsyncIterator[AnyStr]: - while True: - line = await self.readline() - if line: - yield line - else: - break - - async def aclose(self) -> None: - return await to_thread.run_sync(self._fp.close) - - async def read(self, size: int = -1) -> AnyStr: - return await to_thread.run_sync(self._fp.read, size) - - async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: - return await to_thread.run_sync(self._fp.read1, size) - - async def readline(self) -> AnyStr: - return await to_thread.run_sync(self._fp.readline) - - async def readlines(self) -> list[AnyStr]: - return await to_thread.run_sync(self._fp.readlines) - - async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: - return await to_thread.run_sync(self._fp.readinto, b) - - async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: - return await to_thread.run_sync(self._fp.readinto1, b) - - @overload - async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... - - @overload - async def write(self: AsyncFile[str], b: str) -> int: ... - - async def write(self, b: ReadableBuffer | str) -> int: - return await to_thread.run_sync(self._fp.write, b) - - @overload - async def writelines( - self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] - ) -> None: ... - - @overload - async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... - - async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: - return await to_thread.run_sync(self._fp.writelines, lines) - - async def truncate(self, size: int | None = None) -> int: - return await to_thread.run_sync(self._fp.truncate, size) - - async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: - return await to_thread.run_sync(self._fp.seek, offset, whence) - - async def tell(self) -> int: - return await to_thread.run_sync(self._fp.tell) - - async def flush(self) -> None: - return await to_thread.run_sync(self._fp.flush) - - -@overload -async def open_file( - file: str | PathLike[str] | int, - mode: OpenBinaryMode, - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., -) -> AsyncFile[bytes]: ... - - -@overload -async def open_file( - file: str | PathLike[str] | int, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., -) -> AsyncFile[str]: ... - - -async def open_file( - file: str | PathLike[str] | int, - mode: str = "r", - buffering: int = -1, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - closefd: bool = True, - opener: Callable[[str, int], int] | None = None, -) -> AsyncFile[Any]: - """ - Open a file asynchronously. - - The arguments are exactly the same as for the builtin :func:`open`. - - :return: an asynchronous file object - - """ - fp = await to_thread.run_sync( - open, file, mode, buffering, encoding, errors, newline, closefd, opener - ) - return AsyncFile(fp) - - -def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]: - """ - Wrap an existing file as an asynchronous file. - - :param file: an existing file-like object - :return: an asynchronous file object - - """ - return AsyncFile(file) - - -@dataclass(eq=False) -class _PathIterator(AsyncIterator["Path"]): - iterator: Iterator[PathLike[str]] - - async def __anext__(self) -> Path: - nextval = await to_thread.run_sync( - next, self.iterator, None, abandon_on_cancel=True - ) - if nextval is None: - raise StopAsyncIteration from None - - return Path(nextval) - - -class Path: - """ - An asynchronous version of :class:`pathlib.Path`. - - This class cannot be substituted for :class:`pathlib.Path` or - :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` - interface. - - It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for - the deprecated :meth:`~pathlib.Path.link_to` method. - - Some methods may be unavailable or have limited functionality, based on the Python - version: - - * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) - * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) - * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) - * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later) - * :attr:`~pathlib.Path.info` (available on Python 3.14 or later) - * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) - * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only - available on Python 3.13 or later) - * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) - * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) - * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available - on Python 3.12 or later) - * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) - - Any methods that do disk I/O need to be awaited on. These methods are: - - * :meth:`~pathlib.Path.absolute` - * :meth:`~pathlib.Path.chmod` - * :meth:`~pathlib.Path.cwd` - * :meth:`~pathlib.Path.exists` - * :meth:`~pathlib.Path.expanduser` - * :meth:`~pathlib.Path.group` - * :meth:`~pathlib.Path.hardlink_to` - * :meth:`~pathlib.Path.home` - * :meth:`~pathlib.Path.is_block_device` - * :meth:`~pathlib.Path.is_char_device` - * :meth:`~pathlib.Path.is_dir` - * :meth:`~pathlib.Path.is_fifo` - * :meth:`~pathlib.Path.is_file` - * :meth:`~pathlib.Path.is_junction` - * :meth:`~pathlib.Path.is_mount` - * :meth:`~pathlib.Path.is_socket` - * :meth:`~pathlib.Path.is_symlink` - * :meth:`~pathlib.Path.lchmod` - * :meth:`~pathlib.Path.lstat` - * :meth:`~pathlib.Path.mkdir` - * :meth:`~pathlib.Path.open` - * :meth:`~pathlib.Path.owner` - * :meth:`~pathlib.Path.read_bytes` - * :meth:`~pathlib.Path.read_text` - * :meth:`~pathlib.Path.readlink` - * :meth:`~pathlib.Path.rename` - * :meth:`~pathlib.Path.replace` - * :meth:`~pathlib.Path.resolve` - * :meth:`~pathlib.Path.rmdir` - * :meth:`~pathlib.Path.samefile` - * :meth:`~pathlib.Path.stat` - * :meth:`~pathlib.Path.symlink_to` - * :meth:`~pathlib.Path.touch` - * :meth:`~pathlib.Path.unlink` - * :meth:`~pathlib.Path.walk` - * :meth:`~pathlib.Path.write_bytes` - * :meth:`~pathlib.Path.write_text` - - Additionally, the following methods return an async iterator yielding - :class:`~.Path` objects: - - * :meth:`~pathlib.Path.glob` - * :meth:`~pathlib.Path.iterdir` - * :meth:`~pathlib.Path.rglob` - """ - - __slots__ = "_path", "__weakref__" - - __weakref__: Any - - def __init__(self, *args: str | PathLike[str]) -> None: - self._path: Final[pathlib.Path] = pathlib.Path(*args) - - def __fspath__(self) -> str: - return self._path.__fspath__() - - def __str__(self) -> str: - return self._path.__str__() - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.as_posix()!r})" - - def __bytes__(self) -> bytes: - return self._path.__bytes__() - - def __hash__(self) -> int: - return self._path.__hash__() - - def __eq__(self, other: object) -> bool: - target = other._path if isinstance(other, Path) else other - return self._path.__eq__(target) - - def __lt__(self, other: pathlib.PurePath | Path) -> bool: - target = other._path if isinstance(other, Path) else other - return self._path.__lt__(target) - - def __le__(self, other: pathlib.PurePath | Path) -> bool: - target = other._path if isinstance(other, Path) else other - return self._path.__le__(target) - - def __gt__(self, other: pathlib.PurePath | Path) -> bool: - target = other._path if isinstance(other, Path) else other - return self._path.__gt__(target) - - def __ge__(self, other: pathlib.PurePath | Path) -> bool: - target = other._path if isinstance(other, Path) else other - return self._path.__ge__(target) - - def __truediv__(self, other: str | PathLike[str]) -> Path: - return Path(self._path / other) - - def __rtruediv__(self, other: str | PathLike[str]) -> Path: - return Path(other) / self - - @property - def parts(self) -> tuple[str, ...]: - return self._path.parts - - @property - def drive(self) -> str: - return self._path.drive - - @property - def root(self) -> str: - return self._path.root - - @property - def anchor(self) -> str: - return self._path.anchor - - @property - def parents(self) -> Sequence[Path]: - return tuple(Path(p) for p in self._path.parents) - - @property - def parent(self) -> Path: - return Path(self._path.parent) - - @property - def name(self) -> str: - return self._path.name - - @property - def suffix(self) -> str: - return self._path.suffix - - @property - def suffixes(self) -> list[str]: - return self._path.suffixes - - @property - def stem(self) -> str: - return self._path.stem - - async def absolute(self) -> Path: - path = await to_thread.run_sync(self._path.absolute) - return Path(path) - - def as_posix(self) -> str: - return self._path.as_posix() - - def as_uri(self) -> str: - return self._path.as_uri() - - if sys.version_info >= (3, 13): - parser: ClassVar[ModuleType] = pathlib.Path.parser - - @classmethod - def from_uri(cls, uri: str) -> Path: - return Path(pathlib.Path.from_uri(uri)) - - def full_match( - self, path_pattern: str, *, case_sensitive: bool | None = None - ) -> bool: - return self._path.full_match(path_pattern, case_sensitive=case_sensitive) - - def match( - self, path_pattern: str, *, case_sensitive: bool | None = None - ) -> bool: - return self._path.match(path_pattern, case_sensitive=case_sensitive) - else: - - def match(self, path_pattern: str) -> bool: - return self._path.match(path_pattern) - - if sys.version_info >= (3, 14): - - @property - def info(self) -> Any: # TODO: add return type annotation when Typeshed gets it - return self._path.info - - async def copy( - self, - target: str | os.PathLike[str], - *, - follow_symlinks: bool = True, - dirs_exist_ok: bool = False, - preserve_metadata: bool = False, - ) -> Path: - func = partial( - self._path.copy, - follow_symlinks=follow_symlinks, - dirs_exist_ok=dirs_exist_ok, - preserve_metadata=preserve_metadata, - ) - return Path(await to_thread.run_sync(func, target)) - - async def copy_into( - self, - target_dir: str | os.PathLike[str], - *, - follow_symlinks: bool = True, - dirs_exist_ok: bool = False, - preserve_metadata: bool = False, - ) -> Path: - func = partial( - self._path.copy_into, - follow_symlinks=follow_symlinks, - dirs_exist_ok=dirs_exist_ok, - preserve_metadata=preserve_metadata, - ) - return Path(await to_thread.run_sync(func, target_dir)) - - async def move(self, target: str | os.PathLike[str]) -> Path: - # Upstream does not handle anyio.Path properly as a PathLike - target = pathlib.Path(target) - return Path(await to_thread.run_sync(self._path.move, target)) - - async def move_into( - self, - target_dir: str | os.PathLike[str], - ) -> Path: - return Path(await to_thread.run_sync(self._path.move_into, target_dir)) - - def is_relative_to(self, other: str | PathLike[str]) -> bool: - try: - self.relative_to(other) - return True - except ValueError: - return False - - async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: - func = partial(os.chmod, follow_symlinks=follow_symlinks) - return await to_thread.run_sync(func, self._path, mode) - - @classmethod - async def cwd(cls) -> Path: - path = await to_thread.run_sync(pathlib.Path.cwd) - return cls(path) - - async def exists(self) -> bool: - return await to_thread.run_sync(self._path.exists, abandon_on_cancel=True) - - async def expanduser(self) -> Path: - return Path( - await to_thread.run_sync(self._path.expanduser, abandon_on_cancel=True) - ) - - def glob(self, pattern: str) -> AsyncIterator[Path]: - gen = self._path.glob(pattern) - return _PathIterator(gen) - - async def group(self) -> str: - return await to_thread.run_sync(self._path.group, abandon_on_cancel=True) - - async def hardlink_to( - self, target: str | bytes | PathLike[str] | PathLike[bytes] - ) -> None: - if isinstance(target, Path): - target = target._path - - await to_thread.run_sync(os.link, target, self) - - @classmethod - async def home(cls) -> Path: - home_path = await to_thread.run_sync(pathlib.Path.home) - return cls(home_path) - - def is_absolute(self) -> bool: - return self._path.is_absolute() - - async def is_block_device(self) -> bool: - return await to_thread.run_sync( - self._path.is_block_device, abandon_on_cancel=True - ) - - async def is_char_device(self) -> bool: - return await to_thread.run_sync( - self._path.is_char_device, abandon_on_cancel=True - ) - - async def is_dir(self) -> bool: - return await to_thread.run_sync(self._path.is_dir, abandon_on_cancel=True) - - async def is_fifo(self) -> bool: - return await to_thread.run_sync(self._path.is_fifo, abandon_on_cancel=True) - - async def is_file(self) -> bool: - return await to_thread.run_sync(self._path.is_file, abandon_on_cancel=True) - - if sys.version_info >= (3, 12): - - async def is_junction(self) -> bool: - return await to_thread.run_sync(self._path.is_junction) - - async def is_mount(self) -> bool: - return await to_thread.run_sync( - os.path.ismount, self._path, abandon_on_cancel=True - ) - - def is_reserved(self) -> bool: - return self._path.is_reserved() - - async def is_socket(self) -> bool: - return await to_thread.run_sync(self._path.is_socket, abandon_on_cancel=True) - - async def is_symlink(self) -> bool: - return await to_thread.run_sync(self._path.is_symlink, abandon_on_cancel=True) - - async def iterdir(self) -> AsyncIterator[Path]: - gen = ( - self._path.iterdir() - if sys.version_info < (3, 13) - else await to_thread.run_sync(self._path.iterdir, abandon_on_cancel=True) - ) - async for path in _PathIterator(gen): - yield path - - def joinpath(self, *args: str | PathLike[str]) -> Path: - return Path(self._path.joinpath(*args)) - - async def lchmod(self, mode: int) -> None: - await to_thread.run_sync(self._path.lchmod, mode) - - async def lstat(self) -> os.stat_result: - return await to_thread.run_sync(self._path.lstat, abandon_on_cancel=True) - - async def mkdir( - self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False - ) -> None: - await to_thread.run_sync(self._path.mkdir, mode, parents, exist_ok) - - @overload - async def open( - self, - mode: OpenBinaryMode, - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - ) -> AsyncFile[bytes]: ... - - @overload - async def open( - self, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - ) -> AsyncFile[str]: ... - - async def open( - self, - mode: str = "r", - buffering: int = -1, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> AsyncFile[Any]: - fp = await to_thread.run_sync( - self._path.open, mode, buffering, encoding, errors, newline - ) - return AsyncFile(fp) - - async def owner(self) -> str: - return await to_thread.run_sync(self._path.owner, abandon_on_cancel=True) - - async def read_bytes(self) -> bytes: - return await to_thread.run_sync(self._path.read_bytes) - - async def read_text( - self, encoding: str | None = None, errors: str | None = None - ) -> str: - return await to_thread.run_sync(self._path.read_text, encoding, errors) - - if sys.version_info >= (3, 12): - - def relative_to( - self, *other: str | PathLike[str], walk_up: bool = False - ) -> Path: - return Path(self._path.relative_to(*other, walk_up=walk_up)) - - else: - - def relative_to(self, *other: str | PathLike[str]) -> Path: - return Path(self._path.relative_to(*other)) - - async def readlink(self) -> Path: - target = await to_thread.run_sync(os.readlink, self._path) - return Path(target) - - async def rename(self, target: str | pathlib.PurePath | Path) -> Path: - if isinstance(target, Path): - target = target._path - - await to_thread.run_sync(self._path.rename, target) - return Path(target) - - async def replace(self, target: str | pathlib.PurePath | Path) -> Path: - if isinstance(target, Path): - target = target._path - - await to_thread.run_sync(self._path.replace, target) - return Path(target) - - async def resolve(self, strict: bool = False) -> Path: - func = partial(self._path.resolve, strict=strict) - return Path(await to_thread.run_sync(func, abandon_on_cancel=True)) - - def rglob(self, pattern: str) -> AsyncIterator[Path]: - gen = self._path.rglob(pattern) - return _PathIterator(gen) - - async def rmdir(self) -> None: - await to_thread.run_sync(self._path.rmdir) - - async def samefile(self, other_path: str | PathLike[str]) -> bool: - if isinstance(other_path, Path): - other_path = other_path._path - - return await to_thread.run_sync( - self._path.samefile, other_path, abandon_on_cancel=True - ) - - async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: - func = partial(os.stat, follow_symlinks=follow_symlinks) - return await to_thread.run_sync(func, self._path, abandon_on_cancel=True) - - async def symlink_to( - self, - target: str | bytes | PathLike[str] | PathLike[bytes], - target_is_directory: bool = False, - ) -> None: - if isinstance(target, Path): - target = target._path - - await to_thread.run_sync(self._path.symlink_to, target, target_is_directory) - - async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: - await to_thread.run_sync(self._path.touch, mode, exist_ok) - - async def unlink(self, missing_ok: bool = False) -> None: - try: - await to_thread.run_sync(self._path.unlink) - except FileNotFoundError: - if not missing_ok: - raise - - if sys.version_info >= (3, 12): - - async def walk( - self, - top_down: bool = True, - on_error: Callable[[OSError], object] | None = None, - follow_symlinks: bool = False, - ) -> AsyncIterator[tuple[Path, list[str], list[str]]]: - def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: - try: - return next(gen) - except StopIteration: - return None - - gen = self._path.walk(top_down, on_error, follow_symlinks) - while True: - value = await to_thread.run_sync(get_next_value) - if value is None: - return - - root, dirs, paths = value - yield Path(root), dirs, paths - - def with_name(self, name: str) -> Path: - return Path(self._path.with_name(name)) - - def with_stem(self, stem: str) -> Path: - return Path(self._path.with_name(stem + self._path.suffix)) - - def with_suffix(self, suffix: str) -> Path: - return Path(self._path.with_suffix(suffix)) - - def with_segments(self, *pathsegments: str | PathLike[str]) -> Path: - return Path(*pathsegments) - - async def write_bytes(self, data: bytes) -> int: - return await to_thread.run_sync(self._path.write_bytes, data) - - async def write_text( - self, - data: str, - encoding: str | None = None, - errors: str | None = None, - newline: str | None = None, - ) -> int: - # Path.write_text() does not support the "newline" parameter before Python 3.10 - def sync_write_text() -> int: - with self._path.open( - "w", encoding=encoding, errors=errors, newline=newline - ) as fp: - return fp.write(data) - - return await to_thread.run_sync(sync_write_text) - - -PathLike.register(Path) diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_resources.py b/venv/lib/python3.12/site-packages/anyio/_core/_resources.py deleted file mode 100644 index b9a5344..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_resources.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -from ..abc import AsyncResource -from ._tasks import CancelScope - - -async def aclose_forcefully(resource: AsyncResource) -> None: - """ - Close an asynchronous resource in a cancelled scope. - - Doing this closes the resource without waiting on anything. - - :param resource: the resource to close - - """ - with CancelScope() as scope: - scope.cancel() - await resource.aclose() diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_signals.py b/venv/lib/python3.12/site-packages/anyio/_core/_signals.py deleted file mode 100644 index f3451d3..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_signals.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from collections.abc import AsyncIterator -from contextlib import AbstractContextManager -from signal import Signals - -from ._eventloop import get_async_backend - - -def open_signal_receiver( - *signals: Signals, -) -> AbstractContextManager[AsyncIterator[Signals]]: - """ - Start receiving operating system signals. - - :param signals: signals to receive (e.g. ``signal.SIGINT``) - :return: an asynchronous context manager for an asynchronous iterator which yields - signal numbers - - .. warning:: Windows does not support signals natively so it is best to avoid - relying on this in cross-platform applications. - - .. warning:: On asyncio, this permanently replaces any previous signal handler for - the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`. - - """ - return get_async_backend().open_signal_receiver(*signals) diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py b/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py deleted file mode 100644 index 054bcdd..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_sockets.py +++ /dev/null @@ -1,792 +0,0 @@ -from __future__ import annotations - -import errno -import os -import socket -import ssl -import stat -import sys -from collections.abc import Awaitable -from ipaddress import IPv6Address, ip_address -from os import PathLike, chmod -from socket import AddressFamily, SocketKind -from typing import TYPE_CHECKING, Any, Literal, cast, overload - -from .. import to_thread -from ..abc import ( - ConnectedUDPSocket, - ConnectedUNIXDatagramSocket, - IPAddressType, - IPSockAddrType, - SocketListener, - SocketStream, - UDPSocket, - UNIXDatagramSocket, - UNIXSocketStream, -) -from ..streams.stapled import MultiListener -from ..streams.tls import TLSStream -from ._eventloop import get_async_backend -from ._resources import aclose_forcefully -from ._synchronization import Event -from ._tasks import create_task_group, move_on_after - -if TYPE_CHECKING: - from _typeshed import FileDescriptorLike -else: - FileDescriptorLike = object - -if sys.version_info < (3, 11): - from exceptiongroup import ExceptionGroup - -if sys.version_info < (3, 13): - from typing_extensions import deprecated -else: - from warnings import deprecated - -IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 - -AnyIPAddressFamily = Literal[ - AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 -] -IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6] - - -# tls_hostname given -@overload -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = ..., - ssl_context: ssl.SSLContext | None = ..., - tls_standard_compatible: bool = ..., - tls_hostname: str, - happy_eyeballs_delay: float = ..., -) -> TLSStream: ... - - -# ssl_context given -@overload -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = ..., - ssl_context: ssl.SSLContext, - tls_standard_compatible: bool = ..., - tls_hostname: str | None = ..., - happy_eyeballs_delay: float = ..., -) -> TLSStream: ... - - -# tls=True -@overload -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = ..., - tls: Literal[True], - ssl_context: ssl.SSLContext | None = ..., - tls_standard_compatible: bool = ..., - tls_hostname: str | None = ..., - happy_eyeballs_delay: float = ..., -) -> TLSStream: ... - - -# tls=False -@overload -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = ..., - tls: Literal[False], - ssl_context: ssl.SSLContext | None = ..., - tls_standard_compatible: bool = ..., - tls_hostname: str | None = ..., - happy_eyeballs_delay: float = ..., -) -> SocketStream: ... - - -# No TLS arguments -@overload -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = ..., - happy_eyeballs_delay: float = ..., -) -> SocketStream: ... - - -async def connect_tcp( - remote_host: IPAddressType, - remote_port: int, - *, - local_host: IPAddressType | None = None, - tls: bool = False, - ssl_context: ssl.SSLContext | None = None, - tls_standard_compatible: bool = True, - tls_hostname: str | None = None, - happy_eyeballs_delay: float = 0.25, -) -> SocketStream | TLSStream: - """ - Connect to a host using the TCP protocol. - - This function implements the stateless version of the Happy Eyeballs algorithm (RFC - 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses, - each one is tried until one connection attempt succeeds. If the first attempt does - not connected within 250 milliseconds, a second attempt is started using the next - address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if - available) is tried first. - - When the connection has been established, a TLS handshake will be done if either - ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``. - - :param remote_host: the IP address or host name to connect to - :param remote_port: port on the target host to connect to - :param local_host: the interface address or name to bind the socket to before - connecting - :param tls: ``True`` to do a TLS handshake with the connected stream and return a - :class:`~anyio.streams.tls.TLSStream` instead - :param ssl_context: the SSL context object to use (if omitted, a default context is - created) - :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake - before closing the stream and requires that the server does this as well. - Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream. - Some protocols, such as HTTP, require this option to be ``False``. - See :meth:`~ssl.SSLContext.wrap_socket` for details. - :param tls_hostname: host name to check the server certificate against (defaults to - the value of ``remote_host``) - :param happy_eyeballs_delay: delay (in seconds) before starting the next connection - attempt - :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream - :raises OSError: if the connection attempt fails - - """ - # Placed here due to https://github.com/python/mypy/issues/7057 - connected_stream: SocketStream | None = None - - async def try_connect(remote_host: str, event: Event) -> None: - nonlocal connected_stream - try: - stream = await asynclib.connect_tcp(remote_host, remote_port, local_address) - except OSError as exc: - oserrors.append(exc) - return - else: - if connected_stream is None: - connected_stream = stream - tg.cancel_scope.cancel() - else: - await stream.aclose() - finally: - event.set() - - asynclib = get_async_backend() - local_address: IPSockAddrType | None = None - family = socket.AF_UNSPEC - if local_host: - gai_res = await getaddrinfo(str(local_host), None) - family, *_, local_address = gai_res[0] - - target_host = str(remote_host) - try: - addr_obj = ip_address(remote_host) - except ValueError: - addr_obj = None - - if addr_obj is not None: - if isinstance(addr_obj, IPv6Address): - target_addrs = [(socket.AF_INET6, addr_obj.compressed)] - else: - target_addrs = [(socket.AF_INET, addr_obj.compressed)] - else: - # getaddrinfo() will raise an exception if name resolution fails - gai_res = await getaddrinfo( - target_host, remote_port, family=family, type=socket.SOCK_STREAM - ) - - # Organize the list so that the first address is an IPv6 address (if available) - # and the second one is an IPv4 addresses. The rest can be in whatever order. - v6_found = v4_found = False - target_addrs = [] - for af, *rest, sa in gai_res: - if af == socket.AF_INET6 and not v6_found: - v6_found = True - target_addrs.insert(0, (af, sa[0])) - elif af == socket.AF_INET and not v4_found and v6_found: - v4_found = True - target_addrs.insert(1, (af, sa[0])) - else: - target_addrs.append((af, sa[0])) - - oserrors: list[OSError] = [] - try: - async with create_task_group() as tg: - for i, (af, addr) in enumerate(target_addrs): - event = Event() - tg.start_soon(try_connect, addr, event) - with move_on_after(happy_eyeballs_delay): - await event.wait() - - if connected_stream is None: - cause = ( - oserrors[0] - if len(oserrors) == 1 - else ExceptionGroup("multiple connection attempts failed", oserrors) - ) - raise OSError("All connection attempts failed") from cause - finally: - oserrors.clear() - - if tls or tls_hostname or ssl_context: - try: - return await TLSStream.wrap( - connected_stream, - server_side=False, - hostname=tls_hostname or str(remote_host), - ssl_context=ssl_context, - standard_compatible=tls_standard_compatible, - ) - except BaseException: - await aclose_forcefully(connected_stream) - raise - - return connected_stream - - -async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream: - """ - Connect to the given UNIX socket. - - Not available on Windows. - - :param path: path to the socket - :return: a socket stream object - - """ - path = os.fspath(path) - return await get_async_backend().connect_unix(path) - - -async def create_tcp_listener( - *, - local_host: IPAddressType | None = None, - local_port: int = 0, - family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC, - backlog: int = 65536, - reuse_port: bool = False, -) -> MultiListener[SocketStream]: - """ - Create a TCP socket listener. - - :param local_port: port number to listen on - :param local_host: IP address of the interface to listen on. If omitted, listen on - all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address - family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6. - :param family: address family (used if ``local_host`` was omitted) - :param backlog: maximum number of queued incoming connections (up to a maximum of - 2**16, or 65536) - :param reuse_port: ``True`` to allow multiple sockets to bind to the same - address/port (not supported on Windows) - :return: a list of listener objects - - """ - asynclib = get_async_backend() - backlog = min(backlog, 65536) - local_host = str(local_host) if local_host is not None else None - gai_res = await getaddrinfo( - local_host, - local_port, - family=family, - type=socket.SocketKind.SOCK_STREAM if sys.platform == "win32" else 0, - flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, - ) - listeners: list[SocketListener] = [] - try: - # The set() is here to work around a glibc bug: - # https://sourceware.org/bugzilla/show_bug.cgi?id=14969 - sockaddr: tuple[str, int] | tuple[str, int, int, int] - for fam, kind, *_, sockaddr in sorted(set(gai_res)): - # Workaround for an uvloop bug where we don't get the correct scope ID for - # IPv6 link-local addresses when passing type=socket.SOCK_STREAM to - # getaddrinfo(): https://github.com/MagicStack/uvloop/issues/539 - if sys.platform != "win32" and kind is not SocketKind.SOCK_STREAM: - continue - - raw_socket = socket.socket(fam) - raw_socket.setblocking(False) - - # For Windows, enable exclusive address use. For others, enable address - # reuse. - if sys.platform == "win32": - raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) - else: - raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - if reuse_port: - raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - - # If only IPv6 was requested, disable dual stack operation - if fam == socket.AF_INET6: - raw_socket.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) - - # Workaround for #554 - if "%" in sockaddr[0]: - addr, scope_id = sockaddr[0].split("%", 1) - sockaddr = (addr, sockaddr[1], 0, int(scope_id)) - - raw_socket.bind(sockaddr) - raw_socket.listen(backlog) - listener = asynclib.create_tcp_listener(raw_socket) - listeners.append(listener) - except BaseException: - for listener in listeners: - await listener.aclose() - - raise - - return MultiListener(listeners) - - -async def create_unix_listener( - path: str | bytes | PathLike[Any], - *, - mode: int | None = None, - backlog: int = 65536, -) -> SocketListener: - """ - Create a UNIX socket listener. - - Not available on Windows. - - :param path: path of the socket - :param mode: permissions to set on the socket - :param backlog: maximum number of queued incoming connections (up to a maximum of - 2**16, or 65536) - :return: a listener object - - .. versionchanged:: 3.0 - If a socket already exists on the file system in the given path, it will be - removed first. - - """ - backlog = min(backlog, 65536) - raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM) - try: - raw_socket.listen(backlog) - return get_async_backend().create_unix_listener(raw_socket) - except BaseException: - raw_socket.close() - raise - - -async def create_udp_socket( - family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, - *, - local_host: IPAddressType | None = None, - local_port: int = 0, - reuse_port: bool = False, -) -> UDPSocket: - """ - Create a UDP socket. - - If ``port`` has been given, the socket will be bound to this port on the local - machine, making this socket suitable for providing UDP based services. - - :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically - determined from ``local_host`` if omitted - :param local_host: IP address or host name of the local interface to bind to - :param local_port: local port to bind to - :param reuse_port: ``True`` to allow multiple sockets to bind to the same - address/port (not supported on Windows) - :return: a UDP socket - - """ - if family is AddressFamily.AF_UNSPEC and not local_host: - raise ValueError('Either "family" or "local_host" must be given') - - if local_host: - gai_res = await getaddrinfo( - str(local_host), - local_port, - family=family, - type=socket.SOCK_DGRAM, - flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, - ) - family = cast(AnyIPAddressFamily, gai_res[0][0]) - local_address = gai_res[0][-1] - elif family is AddressFamily.AF_INET6: - local_address = ("::", 0) - else: - local_address = ("0.0.0.0", 0) - - sock = await get_async_backend().create_udp_socket( - family, local_address, None, reuse_port - ) - return cast(UDPSocket, sock) - - -async def create_connected_udp_socket( - remote_host: IPAddressType, - remote_port: int, - *, - family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, - local_host: IPAddressType | None = None, - local_port: int = 0, - reuse_port: bool = False, -) -> ConnectedUDPSocket: - """ - Create a connected UDP socket. - - Connected UDP sockets can only communicate with the specified remote host/port, an - any packets sent from other sources are dropped. - - :param remote_host: remote host to set as the default target - :param remote_port: port on the remote host to set as the default target - :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically - determined from ``local_host`` or ``remote_host`` if omitted - :param local_host: IP address or host name of the local interface to bind to - :param local_port: local port to bind to - :param reuse_port: ``True`` to allow multiple sockets to bind to the same - address/port (not supported on Windows) - :return: a connected UDP socket - - """ - local_address = None - if local_host: - gai_res = await getaddrinfo( - str(local_host), - local_port, - family=family, - type=socket.SOCK_DGRAM, - flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, - ) - family = cast(AnyIPAddressFamily, gai_res[0][0]) - local_address = gai_res[0][-1] - - gai_res = await getaddrinfo( - str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM - ) - family = cast(AnyIPAddressFamily, gai_res[0][0]) - remote_address = gai_res[0][-1] - - sock = await get_async_backend().create_udp_socket( - family, local_address, remote_address, reuse_port - ) - return cast(ConnectedUDPSocket, sock) - - -async def create_unix_datagram_socket( - *, - local_path: None | str | bytes | PathLike[Any] = None, - local_mode: int | None = None, -) -> UNIXDatagramSocket: - """ - Create a UNIX datagram socket. - - Not available on Windows. - - If ``local_path`` has been given, the socket will be bound to this path, making this - socket suitable for receiving datagrams from other processes. Other processes can - send datagrams to this socket only if ``local_path`` is set. - - If a socket already exists on the file system in the ``local_path``, it will be - removed first. - - :param local_path: the path on which to bind to - :param local_mode: permissions to set on the local socket - :return: a UNIX datagram socket - - """ - raw_socket = await setup_unix_local_socket( - local_path, local_mode, socket.SOCK_DGRAM - ) - return await get_async_backend().create_unix_datagram_socket(raw_socket, None) - - -async def create_connected_unix_datagram_socket( - remote_path: str | bytes | PathLike[Any], - *, - local_path: None | str | bytes | PathLike[Any] = None, - local_mode: int | None = None, -) -> ConnectedUNIXDatagramSocket: - """ - Create a connected UNIX datagram socket. - - Connected datagram sockets can only communicate with the specified remote path. - - If ``local_path`` has been given, the socket will be bound to this path, making - this socket suitable for receiving datagrams from other processes. Other processes - can send datagrams to this socket only if ``local_path`` is set. - - If a socket already exists on the file system in the ``local_path``, it will be - removed first. - - :param remote_path: the path to set as the default target - :param local_path: the path on which to bind to - :param local_mode: permissions to set on the local socket - :return: a connected UNIX datagram socket - - """ - remote_path = os.fspath(remote_path) - raw_socket = await setup_unix_local_socket( - local_path, local_mode, socket.SOCK_DGRAM - ) - return await get_async_backend().create_unix_datagram_socket( - raw_socket, remote_path - ) - - -async def getaddrinfo( - host: bytes | str | None, - port: str | int | None, - *, - family: int | AddressFamily = 0, - type: int | SocketKind = 0, - proto: int = 0, - flags: int = 0, -) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]: - """ - Look up a numeric IP address given a host name. - - Internationalized domain names are translated according to the (non-transitional) - IDNA 2008 standard. - - .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of - (host, port), unlike what :func:`socket.getaddrinfo` does. - - :param host: host name - :param port: port number - :param family: socket family (`'AF_INET``, ...) - :param type: socket type (``SOCK_STREAM``, ...) - :param proto: protocol number - :param flags: flags to pass to upstream ``getaddrinfo()`` - :return: list of tuples containing (family, type, proto, canonname, sockaddr) - - .. seealso:: :func:`socket.getaddrinfo` - - """ - # Handle unicode hostnames - if isinstance(host, str): - try: - encoded_host: bytes | None = host.encode("ascii") - except UnicodeEncodeError: - import idna - - encoded_host = idna.encode(host, uts46=True) - else: - encoded_host = host - - gai_res = await get_async_backend().getaddrinfo( - encoded_host, port, family=family, type=type, proto=proto, flags=flags - ) - return [ - (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr)) - for family, type, proto, canonname, sockaddr in gai_res - # filter out IPv6 results when IPv6 is disabled - if not isinstance(sockaddr[0], int) - ] - - -def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]: - """ - Look up the host name of an IP address. - - :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) - :param flags: flags to pass to upstream ``getnameinfo()`` - :return: a tuple of (host name, service name) - - .. seealso:: :func:`socket.getnameinfo` - - """ - return get_async_backend().getnameinfo(sockaddr, flags) - - -@deprecated("This function is deprecated; use `wait_readable` instead") -def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: - """ - .. deprecated:: 4.7.0 - Use :func:`wait_readable` instead. - - Wait until the given socket has data to be read. - - .. warning:: Only use this on raw sockets that have not been wrapped by any higher - level constructs like socket streams! - - :param sock: a socket object - :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the - socket to become readable - :raises ~anyio.BusyResourceError: if another task is already waiting for the socket - to become readable - - """ - return get_async_backend().wait_readable(sock.fileno()) - - -@deprecated("This function is deprecated; use `wait_writable` instead") -def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: - """ - .. deprecated:: 4.7.0 - Use :func:`wait_writable` instead. - - Wait until the given socket can be written to. - - This does **NOT** work on Windows when using the asyncio backend with a proactor - event loop (default on py3.8+). - - .. warning:: Only use this on raw sockets that have not been wrapped by any higher - level constructs like socket streams! - - :param sock: a socket object - :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the - socket to become writable - :raises ~anyio.BusyResourceError: if another task is already waiting for the socket - to become writable - - """ - return get_async_backend().wait_writable(sock.fileno()) - - -def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]: - """ - Wait until the given object has data to be read. - - On Unix systems, ``obj`` must either be an integer file descriptor, or else an - object with a ``.fileno()`` method which returns an integer file descriptor. Any - kind of file descriptor can be passed, though the exact semantics will depend on - your kernel. For example, this probably won't do anything useful for on-disk files. - - On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an - object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File - descriptors aren't supported, and neither are handles that refer to anything besides - a ``SOCKET``. - - On backends where this functionality is not natively provided (asyncio - ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread - which is set to shut down when the interpreter shuts down. - - .. warning:: Don't use this on raw sockets that have been wrapped by any higher - level constructs like socket streams! - - :param obj: an object with a ``.fileno()`` method or an integer handle - :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the - object to become readable - :raises ~anyio.BusyResourceError: if another task is already waiting for the object - to become readable - - """ - return get_async_backend().wait_readable(obj) - - -def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]: - """ - Wait until the given object can be written to. - - :param obj: an object with a ``.fileno()`` method or an integer handle - :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the - object to become writable - :raises ~anyio.BusyResourceError: if another task is already waiting for the object - to become writable - - .. seealso:: See the documentation of :func:`wait_readable` for the definition of - ``obj`` and notes on backend compatibility. - - .. warning:: Don't use this on raw sockets that have been wrapped by any higher - level constructs like socket streams! - - """ - return get_async_backend().wait_writable(obj) - - -# -# Private API -# - - -def convert_ipv6_sockaddr( - sockaddr: tuple[str, int, int, int] | tuple[str, int], -) -> tuple[str, int]: - """ - Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format. - - If the scope ID is nonzero, it is added to the address, separated with ``%``. - Otherwise the flow id and scope id are simply cut off from the tuple. - Any other kinds of socket addresses are returned as-is. - - :param sockaddr: the result of :meth:`~socket.socket.getsockname` - :return: the converted socket address - - """ - # This is more complicated than it should be because of MyPy - if isinstance(sockaddr, tuple) and len(sockaddr) == 4: - host, port, flowinfo, scope_id = sockaddr - if scope_id: - # PyPy (as of v7.3.11) leaves the interface name in the result, so - # we discard it and only get the scope ID from the end - # (https://foss.heptapod.net/pypy/pypy/-/issues/3938) - host = host.split("%")[0] - - # Add scope_id to the address - return f"{host}%{scope_id}", port - else: - return host, port - else: - return sockaddr - - -async def setup_unix_local_socket( - path: None | str | bytes | PathLike[Any], - mode: int | None, - socktype: int, -) -> socket.socket: - """ - Create a UNIX local socket object, deleting the socket at the given path if it - exists. - - Not available on Windows. - - :param path: path of the socket - :param mode: permissions to set on the socket - :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM - - """ - path_str: str | None - if path is not None: - path_str = os.fsdecode(path) - - # Linux abstract namespace sockets aren't backed by a concrete file so skip stat call - if not path_str.startswith("\0"): - # Copied from pathlib... - try: - stat_result = os.stat(path) - except OSError as e: - if e.errno not in ( - errno.ENOENT, - errno.ENOTDIR, - errno.EBADF, - errno.ELOOP, - ): - raise - else: - if stat.S_ISSOCK(stat_result.st_mode): - os.unlink(path) - else: - path_str = None - - raw_socket = socket.socket(socket.AF_UNIX, socktype) - raw_socket.setblocking(False) - - if path_str is not None: - try: - await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True) - if mode is not None: - await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True) - except BaseException: - raw_socket.close() - raise - - return raw_socket diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_streams.py b/venv/lib/python3.12/site-packages/anyio/_core/_streams.py deleted file mode 100644 index 6a9814e..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_streams.py +++ /dev/null @@ -1,52 +0,0 @@ -from __future__ import annotations - -import math -from typing import TypeVar -from warnings import warn - -from ..streams.memory import ( - MemoryObjectReceiveStream, - MemoryObjectSendStream, - MemoryObjectStreamState, -) - -T_Item = TypeVar("T_Item") - - -class create_memory_object_stream( - tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]], -): - """ - Create a memory object stream. - - The stream's item type can be annotated like - :func:`create_memory_object_stream[T_Item]`. - - :param max_buffer_size: number of items held in the buffer until ``send()`` starts - blocking - :param item_type: old way of marking the streams with the right generic type for - static typing (does nothing on AnyIO 4) - - .. deprecated:: 4.0 - Use ``create_memory_object_stream[YourItemType](...)`` instead. - :return: a tuple of (send stream, receive stream) - - """ - - def __new__( # type: ignore[misc] - cls, max_buffer_size: float = 0, item_type: object = None - ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]: - if max_buffer_size != math.inf and not isinstance(max_buffer_size, int): - raise ValueError("max_buffer_size must be either an integer or math.inf") - if max_buffer_size < 0: - raise ValueError("max_buffer_size cannot be negative") - if item_type is not None: - warn( - "The item_type argument has been deprecated in AnyIO 4.0. " - "Use create_memory_object_stream[YourItemType](...) instead.", - DeprecationWarning, - stacklevel=2, - ) - - state = MemoryObjectStreamState[T_Item](max_buffer_size) - return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)) diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py b/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py deleted file mode 100644 index 36d9b30..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_subprocesses.py +++ /dev/null @@ -1,202 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import AsyncIterable, Iterable, Mapping, Sequence -from io import BytesIO -from os import PathLike -from subprocess import PIPE, CalledProcessError, CompletedProcess -from typing import IO, Any, Union, cast - -from ..abc import Process -from ._eventloop import get_async_backend -from ._tasks import create_task_group - -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] - - -async def run_process( - command: StrOrBytesPath | Sequence[StrOrBytesPath], - *, - input: bytes | None = None, - stdin: int | IO[Any] | None = None, - stdout: int | IO[Any] | None = PIPE, - stderr: int | IO[Any] | None = PIPE, - check: bool = True, - cwd: StrOrBytesPath | None = None, - env: Mapping[str, str] | None = None, - startupinfo: Any = None, - creationflags: int = 0, - start_new_session: bool = False, - pass_fds: Sequence[int] = (), - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, -) -> CompletedProcess[bytes]: - """ - Run an external command in a subprocess and wait until it completes. - - .. seealso:: :func:`subprocess.run` - - :param command: either a string to pass to the shell, or an iterable of strings - containing the executable name or path and its arguments - :param input: bytes passed to the standard input of the subprocess - :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, - a file-like object, or `None`; ``input`` overrides this - :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, - a file-like object, or `None` - :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, - :data:`subprocess.STDOUT`, a file-like object, or `None` - :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the - process terminates with a return code other than 0 - :param cwd: If not ``None``, change the working directory to this before running the - command - :param env: if not ``None``, this mapping replaces the inherited environment - variables from the parent process - :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used - to specify process startup parameters (Windows only) - :param creationflags: flags that can be used to control the creation of the - subprocess (see :class:`subprocess.Popen` for the specifics) - :param start_new_session: if ``true`` the setsid() system call will be made in the - child process prior to the execution of the subprocess. (POSIX only) - :param pass_fds: sequence of file descriptors to keep open between the parent and - child processes. (POSIX only) - :param user: effective user to run the process as (Python >= 3.9, POSIX only) - :param group: effective group to run the process as (Python >= 3.9, POSIX only) - :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, - POSIX only) - :param umask: if not negative, this umask is applied in the child process before - running the given command (Python >= 3.9, POSIX only) - :return: an object representing the completed process - :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process - exits with a nonzero return code - - """ - - async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: - buffer = BytesIO() - async for chunk in stream: - buffer.write(chunk) - - stream_contents[index] = buffer.getvalue() - - if stdin is not None and input is not None: - raise ValueError("only one of stdin and input is allowed") - - async with await open_process( - command, - stdin=PIPE if input else stdin, - stdout=stdout, - stderr=stderr, - cwd=cwd, - env=env, - startupinfo=startupinfo, - creationflags=creationflags, - start_new_session=start_new_session, - pass_fds=pass_fds, - user=user, - group=group, - extra_groups=extra_groups, - umask=umask, - ) as process: - stream_contents: list[bytes | None] = [None, None] - async with create_task_group() as tg: - if process.stdout: - tg.start_soon(drain_stream, process.stdout, 0) - - if process.stderr: - tg.start_soon(drain_stream, process.stderr, 1) - - if process.stdin and input: - await process.stdin.send(input) - await process.stdin.aclose() - - await process.wait() - - output, errors = stream_contents - if check and process.returncode != 0: - raise CalledProcessError(cast(int, process.returncode), command, output, errors) - - return CompletedProcess(command, cast(int, process.returncode), output, errors) - - -async def open_process( - command: StrOrBytesPath | Sequence[StrOrBytesPath], - *, - stdin: int | IO[Any] | None = PIPE, - stdout: int | IO[Any] | None = PIPE, - stderr: int | IO[Any] | None = PIPE, - cwd: StrOrBytesPath | None = None, - env: Mapping[str, str] | None = None, - startupinfo: Any = None, - creationflags: int = 0, - start_new_session: bool = False, - pass_fds: Sequence[int] = (), - user: str | int | None = None, - group: str | int | None = None, - extra_groups: Iterable[str | int] | None = None, - umask: int = -1, -) -> Process: - """ - Start an external command in a subprocess. - - .. seealso:: :class:`subprocess.Popen` - - :param command: either a string to pass to the shell, or an iterable of strings - containing the executable name or path and its arguments - :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a - file-like object, or ``None`` - :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, - a file-like object, or ``None`` - :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, - :data:`subprocess.STDOUT`, a file-like object, or ``None`` - :param cwd: If not ``None``, the working directory is changed before executing - :param env: If env is not ``None``, it must be a mapping that defines the - environment variables for the new process - :param creationflags: flags that can be used to control the creation of the - subprocess (see :class:`subprocess.Popen` for the specifics) - :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used - to specify process startup parameters (Windows only) - :param start_new_session: if ``true`` the setsid() system call will be made in the - child process prior to the execution of the subprocess. (POSIX only) - :param pass_fds: sequence of file descriptors to keep open between the parent and - child processes. (POSIX only) - :param user: effective user to run the process as (POSIX only) - :param group: effective group to run the process as (POSIX only) - :param extra_groups: supplementary groups to set in the subprocess (POSIX only) - :param umask: if not negative, this umask is applied in the child process before - running the given command (POSIX only) - :return: an asynchronous process object - - """ - kwargs: dict[str, Any] = {} - if user is not None: - kwargs["user"] = user - - if group is not None: - kwargs["group"] = group - - if extra_groups is not None: - kwargs["extra_groups"] = group - - if umask >= 0: - kwargs["umask"] = umask - - return await get_async_backend().open_process( - command, - stdin=stdin, - stdout=stdout, - stderr=stderr, - cwd=cwd, - env=env, - startupinfo=startupinfo, - creationflags=creationflags, - start_new_session=start_new_session, - pass_fds=pass_fds, - **kwargs, - ) diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py b/venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py deleted file mode 100644 index a633132..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_synchronization.py +++ /dev/null @@ -1,732 +0,0 @@ -from __future__ import annotations - -import math -from collections import deque -from dataclasses import dataclass -from types import TracebackType - -from sniffio import AsyncLibraryNotFoundError - -from ..lowlevel import checkpoint -from ._eventloop import get_async_backend -from ._exceptions import BusyResourceError -from ._tasks import CancelScope -from ._testing import TaskInfo, get_current_task - - -@dataclass(frozen=True) -class EventStatistics: - """ - :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` - """ - - tasks_waiting: int - - -@dataclass(frozen=True) -class CapacityLimiterStatistics: - """ - :ivar int borrowed_tokens: number of tokens currently borrowed by tasks - :ivar float total_tokens: total number of available tokens - :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from - this limiter - :ivar int tasks_waiting: number of tasks waiting on - :meth:`~.CapacityLimiter.acquire` or - :meth:`~.CapacityLimiter.acquire_on_behalf_of` - """ - - borrowed_tokens: int - total_tokens: float - borrowers: tuple[object, ...] - tasks_waiting: int - - -@dataclass(frozen=True) -class LockStatistics: - """ - :ivar bool locked: flag indicating if this lock is locked or not - :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the - lock is not held by any task) - :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` - """ - - locked: bool - owner: TaskInfo | None - tasks_waiting: int - - -@dataclass(frozen=True) -class ConditionStatistics: - """ - :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` - :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying - :class:`~.Lock` - """ - - tasks_waiting: int - lock_statistics: LockStatistics - - -@dataclass(frozen=True) -class SemaphoreStatistics: - """ - :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` - - """ - - tasks_waiting: int - - -class Event: - def __new__(cls) -> Event: - try: - return get_async_backend().create_event() - except AsyncLibraryNotFoundError: - return EventAdapter() - - def set(self) -> None: - """Set the flag, notifying all listeners.""" - raise NotImplementedError - - def is_set(self) -> bool: - """Return ``True`` if the flag is set, ``False`` if not.""" - raise NotImplementedError - - async def wait(self) -> None: - """ - Wait until the flag has been set. - - If the flag has already been set when this method is called, it returns - immediately. - - """ - raise NotImplementedError - - def statistics(self) -> EventStatistics: - """Return statistics about the current state of this event.""" - raise NotImplementedError - - -class EventAdapter(Event): - _internal_event: Event | None = None - _is_set: bool = False - - def __new__(cls) -> EventAdapter: - return object.__new__(cls) - - @property - def _event(self) -> Event: - if self._internal_event is None: - self._internal_event = get_async_backend().create_event() - if self._is_set: - self._internal_event.set() - - return self._internal_event - - def set(self) -> None: - if self._internal_event is None: - self._is_set = True - else: - self._event.set() - - def is_set(self) -> bool: - if self._internal_event is None: - return self._is_set - - return self._internal_event.is_set() - - async def wait(self) -> None: - await self._event.wait() - - def statistics(self) -> EventStatistics: - if self._internal_event is None: - return EventStatistics(tasks_waiting=0) - - return self._internal_event.statistics() - - -class Lock: - def __new__(cls, *, fast_acquire: bool = False) -> Lock: - try: - return get_async_backend().create_lock(fast_acquire=fast_acquire) - except AsyncLibraryNotFoundError: - return LockAdapter(fast_acquire=fast_acquire) - - async def __aenter__(self) -> None: - await self.acquire() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.release() - - async def acquire(self) -> None: - """Acquire the lock.""" - raise NotImplementedError - - def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - :raises ~anyio.WouldBlock: if the operation would block - - """ - raise NotImplementedError - - def release(self) -> None: - """Release the lock.""" - raise NotImplementedError - - def locked(self) -> bool: - """Return True if the lock is currently held.""" - raise NotImplementedError - - def statistics(self) -> LockStatistics: - """ - Return statistics about the current state of this lock. - - .. versionadded:: 3.0 - """ - raise NotImplementedError - - -class LockAdapter(Lock): - _internal_lock: Lock | None = None - - def __new__(cls, *, fast_acquire: bool = False) -> LockAdapter: - return object.__new__(cls) - - def __init__(self, *, fast_acquire: bool = False): - self._fast_acquire = fast_acquire - - @property - def _lock(self) -> Lock: - if self._internal_lock is None: - self._internal_lock = get_async_backend().create_lock( - fast_acquire=self._fast_acquire - ) - - return self._internal_lock - - async def __aenter__(self) -> None: - await self._lock.acquire() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - if self._internal_lock is not None: - self._internal_lock.release() - - async def acquire(self) -> None: - """Acquire the lock.""" - await self._lock.acquire() - - def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - :raises ~anyio.WouldBlock: if the operation would block - - """ - self._lock.acquire_nowait() - - def release(self) -> None: - """Release the lock.""" - self._lock.release() - - def locked(self) -> bool: - """Return True if the lock is currently held.""" - return self._lock.locked() - - def statistics(self) -> LockStatistics: - """ - Return statistics about the current state of this lock. - - .. versionadded:: 3.0 - - """ - if self._internal_lock is None: - return LockStatistics(False, None, 0) - - return self._internal_lock.statistics() - - -class Condition: - _owner_task: TaskInfo | None = None - - def __init__(self, lock: Lock | None = None): - self._lock = lock or Lock() - self._waiters: deque[Event] = deque() - - async def __aenter__(self) -> None: - await self.acquire() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.release() - - def _check_acquired(self) -> None: - if self._owner_task != get_current_task(): - raise RuntimeError("The current task is not holding the underlying lock") - - async def acquire(self) -> None: - """Acquire the underlying lock.""" - await self._lock.acquire() - self._owner_task = get_current_task() - - def acquire_nowait(self) -> None: - """ - Acquire the underlying lock, without blocking. - - :raises ~anyio.WouldBlock: if the operation would block - - """ - self._lock.acquire_nowait() - self._owner_task = get_current_task() - - def release(self) -> None: - """Release the underlying lock.""" - self._lock.release() - - def locked(self) -> bool: - """Return True if the lock is set.""" - return self._lock.locked() - - def notify(self, n: int = 1) -> None: - """Notify exactly n listeners.""" - self._check_acquired() - for _ in range(n): - try: - event = self._waiters.popleft() - except IndexError: - break - - event.set() - - def notify_all(self) -> None: - """Notify all the listeners.""" - self._check_acquired() - for event in self._waiters: - event.set() - - self._waiters.clear() - - async def wait(self) -> None: - """Wait for a notification.""" - await checkpoint() - event = Event() - self._waiters.append(event) - self.release() - try: - await event.wait() - except BaseException: - if not event.is_set(): - self._waiters.remove(event) - - raise - finally: - with CancelScope(shield=True): - await self.acquire() - - def statistics(self) -> ConditionStatistics: - """ - Return statistics about the current state of this condition. - - .. versionadded:: 3.0 - """ - return ConditionStatistics(len(self._waiters), self._lock.statistics()) - - -class Semaphore: - def __new__( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> Semaphore: - try: - return get_async_backend().create_semaphore( - initial_value, max_value=max_value, fast_acquire=fast_acquire - ) - except AsyncLibraryNotFoundError: - return SemaphoreAdapter(initial_value, max_value=max_value) - - def __init__( - self, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ): - if not isinstance(initial_value, int): - raise TypeError("initial_value must be an integer") - if initial_value < 0: - raise ValueError("initial_value must be >= 0") - if max_value is not None: - if not isinstance(max_value, int): - raise TypeError("max_value must be an integer or None") - if max_value < initial_value: - raise ValueError( - "max_value must be equal to or higher than initial_value" - ) - - self._fast_acquire = fast_acquire - - async def __aenter__(self) -> Semaphore: - await self.acquire() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.release() - - async def acquire(self) -> None: - """Decrement the semaphore value, blocking if necessary.""" - raise NotImplementedError - - def acquire_nowait(self) -> None: - """ - Acquire the underlying lock, without blocking. - - :raises ~anyio.WouldBlock: if the operation would block - - """ - raise NotImplementedError - - def release(self) -> None: - """Increment the semaphore value.""" - raise NotImplementedError - - @property - def value(self) -> int: - """The current value of the semaphore.""" - raise NotImplementedError - - @property - def max_value(self) -> int | None: - """The maximum value of the semaphore.""" - raise NotImplementedError - - def statistics(self) -> SemaphoreStatistics: - """ - Return statistics about the current state of this semaphore. - - .. versionadded:: 3.0 - """ - raise NotImplementedError - - -class SemaphoreAdapter(Semaphore): - _internal_semaphore: Semaphore | None = None - - def __new__( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> SemaphoreAdapter: - return object.__new__(cls) - - def __init__( - self, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> None: - super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) - self._initial_value = initial_value - self._max_value = max_value - - @property - def _semaphore(self) -> Semaphore: - if self._internal_semaphore is None: - self._internal_semaphore = get_async_backend().create_semaphore( - self._initial_value, max_value=self._max_value - ) - - return self._internal_semaphore - - async def acquire(self) -> None: - await self._semaphore.acquire() - - def acquire_nowait(self) -> None: - self._semaphore.acquire_nowait() - - def release(self) -> None: - self._semaphore.release() - - @property - def value(self) -> int: - if self._internal_semaphore is None: - return self._initial_value - - return self._semaphore.value - - @property - def max_value(self) -> int | None: - return self._max_value - - def statistics(self) -> SemaphoreStatistics: - if self._internal_semaphore is None: - return SemaphoreStatistics(tasks_waiting=0) - - return self._semaphore.statistics() - - -class CapacityLimiter: - def __new__(cls, total_tokens: float) -> CapacityLimiter: - try: - return get_async_backend().create_capacity_limiter(total_tokens) - except AsyncLibraryNotFoundError: - return CapacityLimiterAdapter(total_tokens) - - async def __aenter__(self) -> None: - raise NotImplementedError - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - raise NotImplementedError - - @property - def total_tokens(self) -> float: - """ - The total number of tokens available for borrowing. - - This is a read-write property. If the total number of tokens is increased, the - proportionate number of tasks waiting on this limiter will be granted their - tokens. - - .. versionchanged:: 3.0 - The property is now writable. - - """ - raise NotImplementedError - - @total_tokens.setter - def total_tokens(self, value: float) -> None: - raise NotImplementedError - - @property - def borrowed_tokens(self) -> int: - """The number of tokens that have currently been borrowed.""" - raise NotImplementedError - - @property - def available_tokens(self) -> float: - """The number of tokens currently available to be borrowed""" - raise NotImplementedError - - def acquire_nowait(self) -> None: - """ - Acquire a token for the current task without waiting for one to become - available. - - :raises ~anyio.WouldBlock: if there are no tokens available for borrowing - - """ - raise NotImplementedError - - def acquire_on_behalf_of_nowait(self, borrower: object) -> None: - """ - Acquire a token without waiting for one to become available. - - :param borrower: the entity borrowing a token - :raises ~anyio.WouldBlock: if there are no tokens available for borrowing - - """ - raise NotImplementedError - - async def acquire(self) -> None: - """ - Acquire a token for the current task, waiting if necessary for one to become - available. - - """ - raise NotImplementedError - - async def acquire_on_behalf_of(self, borrower: object) -> None: - """ - Acquire a token, waiting if necessary for one to become available. - - :param borrower: the entity borrowing a token - - """ - raise NotImplementedError - - def release(self) -> None: - """ - Release the token held by the current task. - - :raises RuntimeError: if the current task has not borrowed a token from this - limiter. - - """ - raise NotImplementedError - - def release_on_behalf_of(self, borrower: object) -> None: - """ - Release the token held by the given borrower. - - :raises RuntimeError: if the borrower has not borrowed a token from this - limiter. - - """ - raise NotImplementedError - - def statistics(self) -> CapacityLimiterStatistics: - """ - Return statistics about the current state of this limiter. - - .. versionadded:: 3.0 - - """ - raise NotImplementedError - - -class CapacityLimiterAdapter(CapacityLimiter): - _internal_limiter: CapacityLimiter | None = None - - def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter: - return object.__new__(cls) - - def __init__(self, total_tokens: float) -> None: - self.total_tokens = total_tokens - - @property - def _limiter(self) -> CapacityLimiter: - if self._internal_limiter is None: - self._internal_limiter = get_async_backend().create_capacity_limiter( - self._total_tokens - ) - - return self._internal_limiter - - async def __aenter__(self) -> None: - await self._limiter.__aenter__() - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) - - @property - def total_tokens(self) -> float: - if self._internal_limiter is None: - return self._total_tokens - - return self._internal_limiter.total_tokens - - @total_tokens.setter - def total_tokens(self, value: float) -> None: - if not isinstance(value, int) and value is not math.inf: - raise TypeError("total_tokens must be an int or math.inf") - elif value < 1: - raise ValueError("total_tokens must be >= 1") - - if self._internal_limiter is None: - self._total_tokens = value - return - - self._limiter.total_tokens = value - - @property - def borrowed_tokens(self) -> int: - if self._internal_limiter is None: - return 0 - - return self._internal_limiter.borrowed_tokens - - @property - def available_tokens(self) -> float: - if self._internal_limiter is None: - return self._total_tokens - - return self._internal_limiter.available_tokens - - def acquire_nowait(self) -> None: - self._limiter.acquire_nowait() - - def acquire_on_behalf_of_nowait(self, borrower: object) -> None: - self._limiter.acquire_on_behalf_of_nowait(borrower) - - async def acquire(self) -> None: - await self._limiter.acquire() - - async def acquire_on_behalf_of(self, borrower: object) -> None: - await self._limiter.acquire_on_behalf_of(borrower) - - def release(self) -> None: - self._limiter.release() - - def release_on_behalf_of(self, borrower: object) -> None: - self._limiter.release_on_behalf_of(borrower) - - def statistics(self) -> CapacityLimiterStatistics: - if self._internal_limiter is None: - return CapacityLimiterStatistics( - borrowed_tokens=0, - total_tokens=self.total_tokens, - borrowers=(), - tasks_waiting=0, - ) - - return self._internal_limiter.statistics() - - -class ResourceGuard: - """ - A context manager for ensuring that a resource is only used by a single task at a - time. - - Entering this context manager while the previous has not exited it yet will trigger - :exc:`BusyResourceError`. - - :param action: the action to guard against (visible in the :exc:`BusyResourceError` - when triggered, e.g. "Another task is already {action} this resource") - - .. versionadded:: 4.1 - """ - - __slots__ = "action", "_guarded" - - def __init__(self, action: str = "using"): - self.action: str = action - self._guarded = False - - def __enter__(self) -> None: - if self._guarded: - raise BusyResourceError(self.action) - - self._guarded = True - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self._guarded = False diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_tasks.py b/venv/lib/python3.12/site-packages/anyio/_core/_tasks.py deleted file mode 100644 index fe49015..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_tasks.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import annotations - -import math -from collections.abc import Generator -from contextlib import contextmanager -from types import TracebackType - -from ..abc._tasks import TaskGroup, TaskStatus -from ._eventloop import get_async_backend - - -class _IgnoredTaskStatus(TaskStatus[object]): - def started(self, value: object = None) -> None: - pass - - -TASK_STATUS_IGNORED = _IgnoredTaskStatus() - - -class CancelScope: - """ - Wraps a unit of work that can be made separately cancellable. - - :param deadline: The time (clock value) when this scope is cancelled automatically - :param shield: ``True`` to shield the cancel scope from external cancellation - """ - - def __new__( - cls, *, deadline: float = math.inf, shield: bool = False - ) -> CancelScope: - return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline) - - def cancel(self) -> None: - """Cancel this scope immediately.""" - raise NotImplementedError - - @property - def deadline(self) -> float: - """ - The time (clock value) when this scope is cancelled automatically. - - Will be ``float('inf')`` if no timeout has been set. - - """ - raise NotImplementedError - - @deadline.setter - def deadline(self, value: float) -> None: - raise NotImplementedError - - @property - def cancel_called(self) -> bool: - """``True`` if :meth:`cancel` has been called.""" - raise NotImplementedError - - @property - def cancelled_caught(self) -> bool: - """ - ``True`` if this scope suppressed a cancellation exception it itself raised. - - This is typically used to check if any work was interrupted, or to see if the - scope was cancelled due to its deadline being reached. The value will, however, - only be ``True`` if the cancellation was triggered by the scope itself (and not - an outer scope). - - """ - raise NotImplementedError - - @property - def shield(self) -> bool: - """ - ``True`` if this scope is shielded from external cancellation. - - While a scope is shielded, it will not receive cancellations from outside. - - """ - raise NotImplementedError - - @shield.setter - def shield(self, value: bool) -> None: - raise NotImplementedError - - def __enter__(self) -> CancelScope: - raise NotImplementedError - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool: - raise NotImplementedError - - -@contextmanager -def fail_after( - delay: float | None, shield: bool = False -) -> Generator[CancelScope, None, None]: - """ - Create a context manager which raises a :class:`TimeoutError` if does not finish in - time. - - :param delay: maximum allowed time (in seconds) before raising the exception, or - ``None`` to disable the timeout - :param shield: ``True`` to shield the cancel scope from external cancellation - :return: a context manager that yields a cancel scope - :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] - - """ - current_time = get_async_backend().current_time - deadline = (current_time() + delay) if delay is not None else math.inf - with get_async_backend().create_cancel_scope( - deadline=deadline, shield=shield - ) as cancel_scope: - yield cancel_scope - - if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline: - raise TimeoutError - - -def move_on_after(delay: float | None, shield: bool = False) -> CancelScope: - """ - Create a cancel scope with a deadline that expires after the given delay. - - :param delay: maximum allowed time (in seconds) before exiting the context block, or - ``None`` to disable the timeout - :param shield: ``True`` to shield the cancel scope from external cancellation - :return: a cancel scope - - """ - deadline = ( - (get_async_backend().current_time() + delay) if delay is not None else math.inf - ) - return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield) - - -def current_effective_deadline() -> float: - """ - Return the nearest deadline among all the cancel scopes effective for the current - task. - - :return: a clock value from the event loop's internal clock (or ``float('inf')`` if - there is no deadline in effect, or ``float('-inf')`` if the current scope has - been cancelled) - :rtype: float - - """ - return get_async_backend().current_effective_deadline() - - -def create_task_group() -> TaskGroup: - """ - Create a task group. - - :return: a task group - - """ - return get_async_backend().create_task_group() diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py b/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py deleted file mode 100644 index 26d70ec..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_tempfile.py +++ /dev/null @@ -1,616 +0,0 @@ -from __future__ import annotations - -import os -import sys -import tempfile -from collections.abc import Iterable -from io import BytesIO, TextIOWrapper -from types import TracebackType -from typing import ( - TYPE_CHECKING, - Any, - AnyStr, - Generic, - overload, -) - -from .. import to_thread -from .._core._fileio import AsyncFile -from ..lowlevel import checkpoint_if_cancelled - -if TYPE_CHECKING: - from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer - - -class TemporaryFile(Generic[AnyStr]): - """ - An asynchronous temporary file that is automatically created and cleaned up. - - This class provides an asynchronous context manager interface to a temporary file. - The file is created using Python's standard `tempfile.TemporaryFile` function in a - background thread, and is wrapped as an asynchronous file using `AsyncFile`. - - :param mode: The mode in which the file is opened. Defaults to "w+b". - :param buffering: The buffering policy (-1 means the default buffering). - :param encoding: The encoding used to decode or encode the file. Only applicable in - text mode. - :param newline: Controls how universal newlines mode works (only applicable in text - mode). - :param suffix: The suffix for the temporary file name. - :param prefix: The prefix for the temporary file name. - :param dir: The directory in which the temporary file is created. - :param errors: The error handling scheme used for encoding/decoding errors. - """ - - _async_file: AsyncFile[AnyStr] - - @overload - def __init__( - self: TemporaryFile[bytes], - mode: OpenBinaryMode = ..., - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - *, - errors: str | None = ..., - ): ... - @overload - def __init__( - self: TemporaryFile[str], - mode: OpenTextMode, - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - *, - errors: str | None = ..., - ): ... - - def __init__( - self, - mode: OpenTextMode | OpenBinaryMode = "w+b", - buffering: int = -1, - encoding: str | None = None, - newline: str | None = None, - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, - *, - errors: str | None = None, - ) -> None: - self.mode = mode - self.buffering = buffering - self.encoding = encoding - self.newline = newline - self.suffix: str | None = suffix - self.prefix: str | None = prefix - self.dir: str | None = dir - self.errors = errors - - async def __aenter__(self) -> AsyncFile[AnyStr]: - fp = await to_thread.run_sync( - lambda: tempfile.TemporaryFile( - self.mode, - self.buffering, - self.encoding, - self.newline, - self.suffix, - self.prefix, - self.dir, - errors=self.errors, - ) - ) - self._async_file = AsyncFile(fp) - return self._async_file - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self._async_file.aclose() - - -class NamedTemporaryFile(Generic[AnyStr]): - """ - An asynchronous named temporary file that is automatically created and cleaned up. - - This class provides an asynchronous context manager for a temporary file with a - visible name in the file system. It uses Python's standard - :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with - :class:`AsyncFile` for asynchronous operations. - - :param mode: The mode in which the file is opened. Defaults to "w+b". - :param buffering: The buffering policy (-1 means the default buffering). - :param encoding: The encoding used to decode or encode the file. Only applicable in - text mode. - :param newline: Controls how universal newlines mode works (only applicable in text - mode). - :param suffix: The suffix for the temporary file name. - :param prefix: The prefix for the temporary file name. - :param dir: The directory in which the temporary file is created. - :param delete: Whether to delete the file when it is closed. - :param errors: The error handling scheme used for encoding/decoding errors. - :param delete_on_close: (Python 3.12+) Whether to delete the file on close. - """ - - _async_file: AsyncFile[AnyStr] - - @overload - def __init__( - self: NamedTemporaryFile[bytes], - mode: OpenBinaryMode = ..., - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - delete: bool = ..., - *, - errors: str | None = ..., - delete_on_close: bool = ..., - ): ... - @overload - def __init__( - self: NamedTemporaryFile[str], - mode: OpenTextMode, - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - delete: bool = ..., - *, - errors: str | None = ..., - delete_on_close: bool = ..., - ): ... - - def __init__( - self, - mode: OpenBinaryMode | OpenTextMode = "w+b", - buffering: int = -1, - encoding: str | None = None, - newline: str | None = None, - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, - delete: bool = True, - *, - errors: str | None = None, - delete_on_close: bool = True, - ) -> None: - self._params: dict[str, Any] = { - "mode": mode, - "buffering": buffering, - "encoding": encoding, - "newline": newline, - "suffix": suffix, - "prefix": prefix, - "dir": dir, - "delete": delete, - "errors": errors, - } - if sys.version_info >= (3, 12): - self._params["delete_on_close"] = delete_on_close - - async def __aenter__(self) -> AsyncFile[AnyStr]: - fp = await to_thread.run_sync( - lambda: tempfile.NamedTemporaryFile(**self._params) - ) - self._async_file = AsyncFile(fp) - return self._async_file - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self._async_file.aclose() - - -class SpooledTemporaryFile(AsyncFile[AnyStr]): - """ - An asynchronous spooled temporary file that starts in memory and is spooled to disk. - - This class provides an asynchronous interface to a spooled temporary file, much like - Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous - write operations and provides a method to force a rollover to disk. - - :param max_size: Maximum size in bytes before the file is rolled over to disk. - :param mode: The mode in which the file is opened. Defaults to "w+b". - :param buffering: The buffering policy (-1 means the default buffering). - :param encoding: The encoding used to decode or encode the file (text mode only). - :param newline: Controls how universal newlines mode works (text mode only). - :param suffix: The suffix for the temporary file name. - :param prefix: The prefix for the temporary file name. - :param dir: The directory in which the temporary file is created. - :param errors: The error handling scheme used for encoding/decoding errors. - """ - - _rolled: bool = False - - @overload - def __init__( - self: SpooledTemporaryFile[bytes], - max_size: int = ..., - mode: OpenBinaryMode = ..., - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - *, - errors: str | None = ..., - ): ... - @overload - def __init__( - self: SpooledTemporaryFile[str], - max_size: int = ..., - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: str | None = ..., - newline: str | None = ..., - suffix: str | None = ..., - prefix: str | None = ..., - dir: str | None = ..., - *, - errors: str | None = ..., - ): ... - - def __init__( - self, - max_size: int = 0, - mode: OpenBinaryMode | OpenTextMode = "w+b", - buffering: int = -1, - encoding: str | None = None, - newline: str | None = None, - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, - *, - errors: str | None = None, - ) -> None: - self._tempfile_params: dict[str, Any] = { - "mode": mode, - "buffering": buffering, - "encoding": encoding, - "newline": newline, - "suffix": suffix, - "prefix": prefix, - "dir": dir, - "errors": errors, - } - self._max_size = max_size - if "b" in mode: - super().__init__(BytesIO()) # type: ignore[arg-type] - else: - super().__init__( - TextIOWrapper( # type: ignore[arg-type] - BytesIO(), - encoding=encoding, - errors=errors, - newline=newline, - write_through=True, - ) - ) - - async def aclose(self) -> None: - if not self._rolled: - self._fp.close() - return - - await super().aclose() - - async def _check(self) -> None: - if self._rolled or self._fp.tell() < self._max_size: - return - - await self.rollover() - - async def rollover(self) -> None: - if self._rolled: - return - - self._rolled = True - buffer = self._fp - buffer.seek(0) - self._fp = await to_thread.run_sync( - lambda: tempfile.TemporaryFile(**self._tempfile_params) - ) - await self.write(buffer.read()) - buffer.close() - - @property - def closed(self) -> bool: - return self._fp.closed - - async def read(self, size: int = -1) -> AnyStr: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.read(size) - - return await super().read(size) # type: ignore[return-value] - - async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.read1(size) - - return await super().read1(size) - - async def readline(self) -> AnyStr: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.readline() - - return await super().readline() # type: ignore[return-value] - - async def readlines(self) -> list[AnyStr]: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.readlines() - - return await super().readlines() # type: ignore[return-value] - - async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: - if not self._rolled: - await checkpoint_if_cancelled() - self._fp.readinto(b) - - return await super().readinto(b) - - async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: - if not self._rolled: - await checkpoint_if_cancelled() - self._fp.readinto(b) - - return await super().readinto1(b) - - async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.seek(offset, whence) - - return await super().seek(offset, whence) - - async def tell(self) -> int: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.tell() - - return await super().tell() - - async def truncate(self, size: int | None = None) -> int: - if not self._rolled: - await checkpoint_if_cancelled() - return self._fp.truncate(size) - - return await super().truncate(size) - - @overload - async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ... - @overload - async def write(self: SpooledTemporaryFile[str], b: str) -> int: ... - - async def write(self, b: ReadableBuffer | str) -> int: - """ - Asynchronously write data to the spooled temporary file. - - If the file has not yet been rolled over, the data is written synchronously, - and a rollover is triggered if the size exceeds the maximum size. - - :param s: The data to write. - :return: The number of bytes written. - :raises RuntimeError: If the underlying file is not initialized. - - """ - if not self._rolled: - await checkpoint_if_cancelled() - result = self._fp.write(b) - await self._check() - return result - - return await super().write(b) # type: ignore[misc] - - @overload - async def writelines( - self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer] - ) -> None: ... - @overload - async def writelines( - self: SpooledTemporaryFile[str], lines: Iterable[str] - ) -> None: ... - - async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None: - """ - Asynchronously write a list of lines to the spooled temporary file. - - If the file has not yet been rolled over, the lines are written synchronously, - and a rollover is triggered if the size exceeds the maximum size. - - :param lines: An iterable of lines to write. - :raises RuntimeError: If the underlying file is not initialized. - - """ - if not self._rolled: - await checkpoint_if_cancelled() - result = self._fp.writelines(lines) - await self._check() - return result - - return await super().writelines(lines) # type: ignore[misc] - - -class TemporaryDirectory(Generic[AnyStr]): - """ - An asynchronous temporary directory that is created and cleaned up automatically. - - This class provides an asynchronous context manager for creating a temporary - directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to - perform directory creation and cleanup operations in a background thread. - - :param suffix: Suffix to be added to the temporary directory name. - :param prefix: Prefix to be added to the temporary directory name. - :param dir: The parent directory where the temporary directory is created. - :param ignore_cleanup_errors: Whether to ignore errors during cleanup - (Python 3.10+). - :param delete: Whether to delete the directory upon closing (Python 3.12+). - """ - - def __init__( - self, - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, - *, - ignore_cleanup_errors: bool = False, - delete: bool = True, - ) -> None: - self.suffix: AnyStr | None = suffix - self.prefix: AnyStr | None = prefix - self.dir: AnyStr | None = dir - self.ignore_cleanup_errors = ignore_cleanup_errors - self.delete = delete - - self._tempdir: tempfile.TemporaryDirectory | None = None - - async def __aenter__(self) -> str: - params: dict[str, Any] = { - "suffix": self.suffix, - "prefix": self.prefix, - "dir": self.dir, - } - if sys.version_info >= (3, 10): - params["ignore_cleanup_errors"] = self.ignore_cleanup_errors - - if sys.version_info >= (3, 12): - params["delete"] = self.delete - - self._tempdir = await to_thread.run_sync( - lambda: tempfile.TemporaryDirectory(**params) - ) - return await to_thread.run_sync(self._tempdir.__enter__) - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - if self._tempdir is not None: - await to_thread.run_sync( - self._tempdir.__exit__, exc_type, exc_value, traceback - ) - - async def cleanup(self) -> None: - if self._tempdir is not None: - await to_thread.run_sync(self._tempdir.cleanup) - - -@overload -async def mkstemp( - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, - text: bool = False, -) -> tuple[int, str]: ... - - -@overload -async def mkstemp( - suffix: bytes | None = None, - prefix: bytes | None = None, - dir: bytes | None = None, - text: bool = False, -) -> tuple[int, bytes]: ... - - -async def mkstemp( - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, - text: bool = False, -) -> tuple[int, str | bytes]: - """ - Asynchronously create a temporary file and return an OS-level handle and the file - name. - - This function wraps `tempfile.mkstemp` and executes it in a background thread. - - :param suffix: Suffix to be added to the file name. - :param prefix: Prefix to be added to the file name. - :param dir: Directory in which the temporary file is created. - :param text: Whether the file is opened in text mode. - :return: A tuple containing the file descriptor and the file name. - - """ - return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) - - -@overload -async def mkdtemp( - suffix: str | None = None, - prefix: str | None = None, - dir: str | None = None, -) -> str: ... - - -@overload -async def mkdtemp( - suffix: bytes | None = None, - prefix: bytes | None = None, - dir: bytes | None = None, -) -> bytes: ... - - -async def mkdtemp( - suffix: AnyStr | None = None, - prefix: AnyStr | None = None, - dir: AnyStr | None = None, -) -> str | bytes: - """ - Asynchronously create a temporary directory and return its path. - - This function wraps `tempfile.mkdtemp` and executes it in a background thread. - - :param suffix: Suffix to be added to the directory name. - :param prefix: Prefix to be added to the directory name. - :param dir: Parent directory where the temporary directory is created. - :return: The path of the created temporary directory. - - """ - return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) - - -async def gettempdir() -> str: - """ - Asynchronously return the name of the directory used for temporary files. - - This function wraps `tempfile.gettempdir` and executes it in a background thread. - - :return: The path of the temporary directory as a string. - - """ - return await to_thread.run_sync(tempfile.gettempdir) - - -async def gettempdirb() -> bytes: - """ - Asynchronously return the name of the directory used for temporary files in bytes. - - This function wraps `tempfile.gettempdirb` and executes it in a background thread. - - :return: The path of the temporary directory as bytes. - - """ - return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_testing.py b/venv/lib/python3.12/site-packages/anyio/_core/_testing.py deleted file mode 100644 index 9e28b22..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_testing.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from collections.abc import Awaitable, Generator -from typing import Any, cast - -from ._eventloop import get_async_backend - - -class TaskInfo: - """ - Represents an asynchronous task. - - :ivar int id: the unique identifier of the task - :ivar parent_id: the identifier of the parent task, if any - :vartype parent_id: Optional[int] - :ivar str name: the description of the task (if any) - :ivar ~collections.abc.Coroutine coro: the coroutine object of the task - """ - - __slots__ = "_name", "id", "parent_id", "name", "coro" - - def __init__( - self, - id: int, - parent_id: int | None, - name: str | None, - coro: Generator[Any, Any, Any] | Awaitable[Any], - ): - func = get_current_task - self._name = f"{func.__module__}.{func.__qualname__}" - self.id: int = id - self.parent_id: int | None = parent_id - self.name: str | None = name - self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro - - def __eq__(self, other: object) -> bool: - if isinstance(other, TaskInfo): - return self.id == other.id - - return NotImplemented - - def __hash__(self) -> int: - return hash(self.id) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})" - - def has_pending_cancellation(self) -> bool: - """ - Return ``True`` if the task has a cancellation pending, ``False`` otherwise. - - """ - return False - - -def get_current_task() -> TaskInfo: - """ - Return the current task. - - :return: a representation of the current task - - """ - return get_async_backend().get_current_task() - - -def get_running_tasks() -> list[TaskInfo]: - """ - Return a list of running tasks in the current event loop. - - :return: a list of task info objects - - """ - return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) - - -async def wait_all_tasks_blocked() -> None: - """Wait until all other tasks are waiting for something.""" - await get_async_backend().wait_all_tasks_blocked() diff --git a/venv/lib/python3.12/site-packages/anyio/_core/_typedattr.py b/venv/lib/python3.12/site-packages/anyio/_core/_typedattr.py deleted file mode 100644 index f358a44..0000000 --- a/venv/lib/python3.12/site-packages/anyio/_core/_typedattr.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Mapping -from typing import Any, TypeVar, final, overload - -from ._exceptions import TypedAttributeLookupError - -T_Attr = TypeVar("T_Attr") -T_Default = TypeVar("T_Default") -undefined = object() - - -def typed_attribute() -> Any: - """Return a unique object, used to mark typed attributes.""" - return object() - - -class TypedAttributeSet: - """ - Superclass for typed attribute collections. - - Checks that every public attribute of every subclass has a type annotation. - """ - - def __init_subclass__(cls) -> None: - annotations: dict[str, Any] = getattr(cls, "__annotations__", {}) - for attrname in dir(cls): - if not attrname.startswith("_") and attrname not in annotations: - raise TypeError( - f"Attribute {attrname!r} is missing its type annotation" - ) - - super().__init_subclass__() - - -class TypedAttributeProvider: - """Base class for classes that wish to provide typed extra attributes.""" - - @property - def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]: - """ - A mapping of the extra attributes to callables that return the corresponding - values. - - If the provider wraps another provider, the attributes from that wrapper should - also be included in the returned mapping (but the wrapper may override the - callables from the wrapped instance). - - """ - return {} - - @overload - def extra(self, attribute: T_Attr) -> T_Attr: ... - - @overload - def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ... - - @final - def extra(self, attribute: Any, default: object = undefined) -> object: - """ - extra(attribute, default=undefined) - - Return the value of the given typed extra attribute. - - :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to - look for - :param default: the value that should be returned if no value is found for the - attribute - :raises ~anyio.TypedAttributeLookupError: if the search failed and no default - value was given - - """ - try: - getter = self.extra_attributes[attribute] - except KeyError: - if default is undefined: - raise TypedAttributeLookupError("Attribute not found") from None - else: - return default - - return getter() diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__init__.py b/venv/lib/python3.12/site-packages/anyio/abc/__init__.py deleted file mode 100644 index 3d3b61c..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from ._eventloop import AsyncBackend as AsyncBackend -from ._resources import AsyncResource as AsyncResource -from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket -from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket -from ._sockets import IPAddressType as IPAddressType -from ._sockets import IPSockAddrType as IPSockAddrType -from ._sockets import SocketAttribute as SocketAttribute -from ._sockets import SocketListener as SocketListener -from ._sockets import SocketStream as SocketStream -from ._sockets import UDPPacketType as UDPPacketType -from ._sockets import UDPSocket as UDPSocket -from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType -from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket -from ._sockets import UNIXSocketStream as UNIXSocketStream -from ._streams import AnyByteReceiveStream as AnyByteReceiveStream -from ._streams import AnyByteSendStream as AnyByteSendStream -from ._streams import AnyByteStream as AnyByteStream -from ._streams import AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream -from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream -from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream -from ._streams import ByteReceiveStream as ByteReceiveStream -from ._streams import ByteSendStream as ByteSendStream -from ._streams import ByteStream as ByteStream -from ._streams import Listener as Listener -from ._streams import ObjectReceiveStream as ObjectReceiveStream -from ._streams import ObjectSendStream as ObjectSendStream -from ._streams import ObjectStream as ObjectStream -from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream -from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream -from ._streams import UnreliableObjectStream as UnreliableObjectStream -from ._subprocesses import Process as Process -from ._tasks import TaskGroup as TaskGroup -from ._tasks import TaskStatus as TaskStatus -from ._testing import TestRunner as TestRunner - -# Re-exported here, for backwards compatibility -# isort: off -from .._core._synchronization import ( - CapacityLimiter as CapacityLimiter, - Condition as Condition, - Event as Event, - Lock as Lock, - Semaphore as Semaphore, -) -from .._core._tasks import CancelScope as CancelScope -from ..from_thread import BlockingPortal as BlockingPortal - -# Re-export imports so they look like they live directly in this package -for __value in list(locals().values()): - if getattr(__value, "__module__", "").startswith("anyio.abc."): - __value.__module__ = __name__ - -del __value diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index e4ed155..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc deleted file mode 100644 index 5a43c9b..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_eventloop.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc deleted file mode 100644 index 12ec440..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_resources.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc deleted file mode 100644 index b8139fc..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_sockets.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc deleted file mode 100644 index 7ca50a7..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_streams.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc deleted file mode 100644 index 74127e7..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc deleted file mode 100644 index 88df606..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_tasks.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc deleted file mode 100644 index 7bbd5f5..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/abc/__pycache__/_testing.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py b/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py deleted file mode 100644 index 4cfce83..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_eventloop.py +++ /dev/null @@ -1,376 +0,0 @@ -from __future__ import annotations - -import math -import sys -from abc import ABCMeta, abstractmethod -from collections.abc import AsyncIterator, Awaitable, Callable, Sequence -from contextlib import AbstractContextManager -from os import PathLike -from signal import Signals -from socket import AddressFamily, SocketKind, socket -from typing import ( - IO, - TYPE_CHECKING, - Any, - TypeVar, - Union, - overload, -) - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -if sys.version_info >= (3, 10): - from typing import TypeAlias -else: - from typing_extensions import TypeAlias - -if TYPE_CHECKING: - from _typeshed import HasFileno - - from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore - from .._core._tasks import CancelScope - from .._core._testing import TaskInfo - from ..from_thread import BlockingPortal - from ._sockets import ( - ConnectedUDPSocket, - ConnectedUNIXDatagramSocket, - IPSockAddrType, - SocketListener, - SocketStream, - UDPSocket, - UNIXDatagramSocket, - UNIXSocketStream, - ) - from ._subprocesses import Process - from ._tasks import TaskGroup - from ._testing import TestRunner - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") -StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] - - -class AsyncBackend(metaclass=ABCMeta): - @classmethod - @abstractmethod - def run( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - options: dict[str, Any], - ) -> T_Retval: - """ - Run the given coroutine function in an asynchronous event loop. - - The current thread must not be already running an event loop. - - :param func: a coroutine function - :param args: positional arguments to ``func`` - :param kwargs: positional arguments to ``func`` - :param options: keyword arguments to call the backend ``run()`` implementation - with - :return: the return value of the coroutine function - """ - - @classmethod - @abstractmethod - def current_token(cls) -> object: - """ - - :return: - """ - - @classmethod - @abstractmethod - def current_time(cls) -> float: - """ - Return the current value of the event loop's internal clock. - - :return: the clock value (seconds) - """ - - @classmethod - @abstractmethod - def cancelled_exception_class(cls) -> type[BaseException]: - """Return the exception class that is raised in a task if it's cancelled.""" - - @classmethod - @abstractmethod - async def checkpoint(cls) -> None: - """ - Check if the task has been cancelled, and allow rescheduling of other tasks. - - This is effectively the same as running :meth:`checkpoint_if_cancelled` and then - :meth:`cancel_shielded_checkpoint`. - """ - - @classmethod - async def checkpoint_if_cancelled(cls) -> None: - """ - Check if the current task group has been cancelled. - - This will check if the task has been cancelled, but will not allow other tasks - to be scheduled if not. - - """ - if cls.current_effective_deadline() == -math.inf: - await cls.checkpoint() - - @classmethod - async def cancel_shielded_checkpoint(cls) -> None: - """ - Allow the rescheduling of other tasks. - - This will give other tasks the opportunity to run, but without checking if the - current task group has been cancelled, unlike with :meth:`checkpoint`. - - """ - with cls.create_cancel_scope(shield=True): - await cls.sleep(0) - - @classmethod - @abstractmethod - async def sleep(cls, delay: float) -> None: - """ - Pause the current task for the specified duration. - - :param delay: the duration, in seconds - """ - - @classmethod - @abstractmethod - def create_cancel_scope( - cls, *, deadline: float = math.inf, shield: bool = False - ) -> CancelScope: - pass - - @classmethod - @abstractmethod - def current_effective_deadline(cls) -> float: - """ - Return the nearest deadline among all the cancel scopes effective for the - current task. - - :return: - - a clock value from the event loop's internal clock - - ``inf`` if there is no deadline in effect - - ``-inf`` if the current scope has been cancelled - :rtype: float - """ - - @classmethod - @abstractmethod - def create_task_group(cls) -> TaskGroup: - pass - - @classmethod - @abstractmethod - def create_event(cls) -> Event: - pass - - @classmethod - @abstractmethod - def create_lock(cls, *, fast_acquire: bool) -> Lock: - pass - - @classmethod - @abstractmethod - def create_semaphore( - cls, - initial_value: int, - *, - max_value: int | None = None, - fast_acquire: bool = False, - ) -> Semaphore: - pass - - @classmethod - @abstractmethod - def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: - pass - - @classmethod - @abstractmethod - async def run_sync_in_worker_thread( - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - abandon_on_cancel: bool = False, - limiter: CapacityLimiter | None = None, - ) -> T_Retval: - pass - - @classmethod - @abstractmethod - def check_cancelled(cls) -> None: - pass - - @classmethod - @abstractmethod - def run_async_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - pass - - @classmethod - @abstractmethod - def run_sync_from_thread( - cls, - func: Callable[[Unpack[PosArgsT]], T_Retval], - args: tuple[Unpack[PosArgsT]], - token: object, - ) -> T_Retval: - pass - - @classmethod - @abstractmethod - def create_blocking_portal(cls) -> BlockingPortal: - pass - - @classmethod - @abstractmethod - async def open_process( - cls, - command: StrOrBytesPath | Sequence[StrOrBytesPath], - *, - stdin: int | IO[Any] | None, - stdout: int | IO[Any] | None, - stderr: int | IO[Any] | None, - **kwargs: Any, - ) -> Process: - pass - - @classmethod - @abstractmethod - def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None: - pass - - @classmethod - @abstractmethod - async def connect_tcp( - cls, host: str, port: int, local_address: IPSockAddrType | None = None - ) -> SocketStream: - pass - - @classmethod - @abstractmethod - async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream: - pass - - @classmethod - @abstractmethod - def create_tcp_listener(cls, sock: socket) -> SocketListener: - pass - - @classmethod - @abstractmethod - def create_unix_listener(cls, sock: socket) -> SocketListener: - pass - - @classmethod - @abstractmethod - async def create_udp_socket( - cls, - family: AddressFamily, - local_address: IPSockAddrType | None, - remote_address: IPSockAddrType | None, - reuse_port: bool, - ) -> UDPSocket | ConnectedUDPSocket: - pass - - @classmethod - @overload - async def create_unix_datagram_socket( - cls, raw_socket: socket, remote_path: None - ) -> UNIXDatagramSocket: ... - - @classmethod - @overload - async def create_unix_datagram_socket( - cls, raw_socket: socket, remote_path: str | bytes - ) -> ConnectedUNIXDatagramSocket: ... - - @classmethod - @abstractmethod - async def create_unix_datagram_socket( - cls, raw_socket: socket, remote_path: str | bytes | None - ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket: - pass - - @classmethod - @abstractmethod - async def getaddrinfo( - cls, - host: bytes | str | None, - port: str | int | None, - *, - family: int | AddressFamily = 0, - type: int | SocketKind = 0, - proto: int = 0, - flags: int = 0, - ) -> Sequence[ - tuple[ - AddressFamily, - SocketKind, - int, - str, - tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], - ] - ]: - pass - - @classmethod - @abstractmethod - async def getnameinfo( - cls, sockaddr: IPSockAddrType, flags: int = 0 - ) -> tuple[str, str]: - pass - - @classmethod - @abstractmethod - async def wait_readable(cls, obj: HasFileno | int) -> None: - pass - - @classmethod - @abstractmethod - async def wait_writable(cls, obj: HasFileno | int) -> None: - pass - - @classmethod - @abstractmethod - def current_default_thread_limiter(cls) -> CapacityLimiter: - pass - - @classmethod - @abstractmethod - def open_signal_receiver( - cls, *signals: Signals - ) -> AbstractContextManager[AsyncIterator[Signals]]: - pass - - @classmethod - @abstractmethod - def get_current_task(cls) -> TaskInfo: - pass - - @classmethod - @abstractmethod - def get_running_tasks(cls) -> Sequence[TaskInfo]: - pass - - @classmethod - @abstractmethod - async def wait_all_tasks_blocked(cls) -> None: - pass - - @classmethod - @abstractmethod - def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: - pass diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_resources.py b/venv/lib/python3.12/site-packages/anyio/abc/_resources.py deleted file mode 100644 index 10df115..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_resources.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from abc import ABCMeta, abstractmethod -from types import TracebackType -from typing import TypeVar - -T = TypeVar("T") - - -class AsyncResource(metaclass=ABCMeta): - """ - Abstract base class for all closeable asynchronous resources. - - Works as an asynchronous context manager which returns the instance itself on enter, - and calls :meth:`aclose` on exit. - """ - - __slots__ = () - - async def __aenter__(self: T) -> T: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - await self.aclose() - - @abstractmethod - async def aclose(self) -> None: - """Close the resource.""" diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py b/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py deleted file mode 100644 index 1c6a450..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_sockets.py +++ /dev/null @@ -1,194 +0,0 @@ -from __future__ import annotations - -import socket -from abc import abstractmethod -from collections.abc import Callable, Collection, Mapping -from contextlib import AsyncExitStack -from io import IOBase -from ipaddress import IPv4Address, IPv6Address -from socket import AddressFamily -from types import TracebackType -from typing import Any, TypeVar, Union - -from .._core._typedattr import ( - TypedAttributeProvider, - TypedAttributeSet, - typed_attribute, -) -from ._streams import ByteStream, Listener, UnreliableObjectStream -from ._tasks import TaskGroup - -IPAddressType = Union[str, IPv4Address, IPv6Address] -IPSockAddrType = tuple[str, int] -SockAddrType = Union[IPSockAddrType, str] -UDPPacketType = tuple[bytes, IPSockAddrType] -UNIXDatagramPacketType = tuple[bytes, str] -T_Retval = TypeVar("T_Retval") - - -class _NullAsyncContextManager: - async def __aenter__(self) -> None: - pass - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - return None - - -class SocketAttribute(TypedAttributeSet): - #: the address family of the underlying socket - family: AddressFamily = typed_attribute() - #: the local socket address of the underlying socket - local_address: SockAddrType = typed_attribute() - #: for IP addresses, the local port the underlying socket is bound to - local_port: int = typed_attribute() - #: the underlying stdlib socket object - raw_socket: socket.socket = typed_attribute() - #: the remote address the underlying socket is connected to - remote_address: SockAddrType = typed_attribute() - #: for IP addresses, the remote port the underlying socket is connected to - remote_port: int = typed_attribute() - - -class _SocketProvider(TypedAttributeProvider): - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - from .._core._sockets import convert_ipv6_sockaddr as convert - - attributes: dict[Any, Callable[[], Any]] = { - SocketAttribute.family: lambda: self._raw_socket.family, - SocketAttribute.local_address: lambda: convert( - self._raw_socket.getsockname() - ), - SocketAttribute.raw_socket: lambda: self._raw_socket, - } - try: - peername: tuple[str, int] | None = convert(self._raw_socket.getpeername()) - except OSError: - peername = None - - # Provide the remote address for connected sockets - if peername is not None: - attributes[SocketAttribute.remote_address] = lambda: peername - - # Provide local and remote ports for IP based sockets - if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6): - attributes[SocketAttribute.local_port] = ( - lambda: self._raw_socket.getsockname()[1] - ) - if peername is not None: - remote_port = peername[1] - attributes[SocketAttribute.remote_port] = lambda: remote_port - - return attributes - - @property - @abstractmethod - def _raw_socket(self) -> socket.socket: - pass - - -class SocketStream(ByteStream, _SocketProvider): - """ - Transports bytes over a socket. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ - - -class UNIXSocketStream(SocketStream): - @abstractmethod - async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: - """ - Send file descriptors along with a message to the peer. - - :param message: a non-empty bytestring - :param fds: a collection of files (either numeric file descriptors or open file - or socket objects) - """ - - @abstractmethod - async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: - """ - Receive file descriptors along with a message from the peer. - - :param msglen: length of the message to expect from the peer - :param maxfds: maximum number of file descriptors to expect from the peer - :return: a tuple of (message, file descriptors) - """ - - -class SocketListener(Listener[SocketStream], _SocketProvider): - """ - Listens to incoming socket connections. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ - - @abstractmethod - async def accept(self) -> SocketStream: - """Accept an incoming connection.""" - - async def serve( - self, - handler: Callable[[SocketStream], Any], - task_group: TaskGroup | None = None, - ) -> None: - from .. import create_task_group - - async with AsyncExitStack() as stack: - if task_group is None: - task_group = await stack.enter_async_context(create_task_group()) - - while True: - stream = await self.accept() - task_group.start_soon(handler, stream) - - -class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider): - """ - Represents an unconnected UDP socket. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ - - async def sendto(self, data: bytes, host: str, port: int) -> None: - """ - Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))). - - """ - return await self.send((data, (host, port))) - - -class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider): - """ - Represents an connected UDP socket. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ - - -class UNIXDatagramSocket( - UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider -): - """ - Represents an unconnected Unix datagram socket. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ - - async def sendto(self, data: bytes, path: str) -> None: - """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path)).""" - return await self.send((data, path)) - - -class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider): - """ - Represents a connected Unix datagram socket. - - Supports all relevant extra attributes from :class:`~SocketAttribute`. - """ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_streams.py b/venv/lib/python3.12/site-packages/anyio/abc/_streams.py deleted file mode 100644 index f11d97b..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_streams.py +++ /dev/null @@ -1,203 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from collections.abc import Callable -from typing import Any, Generic, TypeVar, Union - -from .._core._exceptions import EndOfStream -from .._core._typedattr import TypedAttributeProvider -from ._resources import AsyncResource -from ._tasks import TaskGroup - -T_Item = TypeVar("T_Item") -T_co = TypeVar("T_co", covariant=True) -T_contra = TypeVar("T_contra", contravariant=True) - - -class UnreliableObjectReceiveStream( - Generic[T_co], AsyncResource, TypedAttributeProvider -): - """ - An interface for receiving objects. - - This interface makes no guarantees that the received messages arrive in the order in - which they were sent, or that no messages are missed. - - Asynchronously iterating over objects of this type will yield objects matching the - given type parameter. - """ - - def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]: - return self - - async def __anext__(self) -> T_co: - try: - return await self.receive() - except EndOfStream: - raise StopAsyncIteration - - @abstractmethod - async def receive(self) -> T_co: - """ - Receive the next item. - - :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly - closed - :raises ~anyio.EndOfStream: if this stream has been closed from the other end - :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable - due to external causes - """ - - -class UnreliableObjectSendStream( - Generic[T_contra], AsyncResource, TypedAttributeProvider -): - """ - An interface for sending objects. - - This interface makes no guarantees that the messages sent will reach the - recipient(s) in the same order in which they were sent, or at all. - """ - - @abstractmethod - async def send(self, item: T_contra) -> None: - """ - Send an item to the peer(s). - - :param item: the item to send - :raises ~anyio.ClosedResourceError: if the send stream has been explicitly - closed - :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable - due to external causes - """ - - -class UnreliableObjectStream( - UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item] -): - """ - A bidirectional message stream which does not guarantee the order or reliability of - message delivery. - """ - - -class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]): - """ - A receive message stream which guarantees that messages are received in the same - order in which they were sent, and that no messages are missed. - """ - - -class ObjectSendStream(UnreliableObjectSendStream[T_contra]): - """ - A send message stream which guarantees that messages are delivered in the same order - in which they were sent, without missing any messages in the middle. - """ - - -class ObjectStream( - ObjectReceiveStream[T_Item], - ObjectSendStream[T_Item], - UnreliableObjectStream[T_Item], -): - """ - A bidirectional message stream which guarantees the order and reliability of message - delivery. - """ - - @abstractmethod - async def send_eof(self) -> None: - """ - Send an end-of-file indication to the peer. - - You should not try to send any further data to this stream after calling this - method. This method is idempotent (does nothing on successive calls). - """ - - -class ByteReceiveStream(AsyncResource, TypedAttributeProvider): - """ - An interface for receiving bytes from a single peer. - - Iterating this byte stream will yield a byte string of arbitrary length, but no more - than 65536 bytes. - """ - - def __aiter__(self) -> ByteReceiveStream: - return self - - async def __anext__(self) -> bytes: - try: - return await self.receive() - except EndOfStream: - raise StopAsyncIteration - - @abstractmethod - async def receive(self, max_bytes: int = 65536) -> bytes: - """ - Receive at most ``max_bytes`` bytes from the peer. - - .. note:: Implementers of this interface should not return an empty - :class:`bytes` object, and users should ignore them. - - :param max_bytes: maximum number of bytes to receive - :return: the received bytes - :raises ~anyio.EndOfStream: if this stream has been closed from the other end - """ - - -class ByteSendStream(AsyncResource, TypedAttributeProvider): - """An interface for sending bytes to a single peer.""" - - @abstractmethod - async def send(self, item: bytes) -> None: - """ - Send the given bytes to the peer. - - :param item: the bytes to send - """ - - -class ByteStream(ByteReceiveStream, ByteSendStream): - """A bidirectional byte stream.""" - - @abstractmethod - async def send_eof(self) -> None: - """ - Send an end-of-file indication to the peer. - - You should not try to send any further data to this stream after calling this - method. This method is idempotent (does nothing on successive calls). - """ - - -#: Type alias for all unreliable bytes-oriented receive streams. -AnyUnreliableByteReceiveStream = Union[ - UnreliableObjectReceiveStream[bytes], ByteReceiveStream -] -#: Type alias for all unreliable bytes-oriented send streams. -AnyUnreliableByteSendStream = Union[UnreliableObjectSendStream[bytes], ByteSendStream] -#: Type alias for all unreliable bytes-oriented streams. -AnyUnreliableByteStream = Union[UnreliableObjectStream[bytes], ByteStream] -#: Type alias for all bytes-oriented receive streams. -AnyByteReceiveStream = Union[ObjectReceiveStream[bytes], ByteReceiveStream] -#: Type alias for all bytes-oriented send streams. -AnyByteSendStream = Union[ObjectSendStream[bytes], ByteSendStream] -#: Type alias for all bytes-oriented streams. -AnyByteStream = Union[ObjectStream[bytes], ByteStream] - - -class Listener(Generic[T_co], AsyncResource, TypedAttributeProvider): - """An interface for objects that let you accept incoming connections.""" - - @abstractmethod - async def serve( - self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None - ) -> None: - """ - Accept incoming connections as they come in and start tasks to handle them. - - :param handler: a callable that will be used to handle each accepted connection - :param task_group: the task group that will be used to start tasks for handling - each accepted connection (if omitted, an ad-hoc task group will be created) - """ diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py b/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py deleted file mode 100644 index ce0564c..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_subprocesses.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from signal import Signals - -from ._resources import AsyncResource -from ._streams import ByteReceiveStream, ByteSendStream - - -class Process(AsyncResource): - """An asynchronous version of :class:`subprocess.Popen`.""" - - @abstractmethod - async def wait(self) -> int: - """ - Wait until the process exits. - - :return: the exit code of the process - """ - - @abstractmethod - def terminate(self) -> None: - """ - Terminates the process, gracefully if possible. - - On Windows, this calls ``TerminateProcess()``. - On POSIX systems, this sends ``SIGTERM`` to the process. - - .. seealso:: :meth:`subprocess.Popen.terminate` - """ - - @abstractmethod - def kill(self) -> None: - """ - Kills the process. - - On Windows, this calls ``TerminateProcess()``. - On POSIX systems, this sends ``SIGKILL`` to the process. - - .. seealso:: :meth:`subprocess.Popen.kill` - """ - - @abstractmethod - def send_signal(self, signal: Signals) -> None: - """ - Send a signal to the subprocess. - - .. seealso:: :meth:`subprocess.Popen.send_signal` - - :param signal: the signal number (e.g. :data:`signal.SIGHUP`) - """ - - @property - @abstractmethod - def pid(self) -> int: - """The process ID of the process.""" - - @property - @abstractmethod - def returncode(self) -> int | None: - """ - The return code of the process. If the process has not yet terminated, this will - be ``None``. - """ - - @property - @abstractmethod - def stdin(self) -> ByteSendStream | None: - """The stream for the standard input of the process.""" - - @property - @abstractmethod - def stdout(self) -> ByteReceiveStream | None: - """The stream for the standard output of the process.""" - - @property - @abstractmethod - def stderr(self) -> ByteReceiveStream | None: - """The stream for the standard error output of the process.""" diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_tasks.py b/venv/lib/python3.12/site-packages/anyio/abc/_tasks.py deleted file mode 100644 index f6e5c40..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_tasks.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import annotations - -import sys -from abc import ABCMeta, abstractmethod -from collections.abc import Awaitable, Callable -from types import TracebackType -from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -if TYPE_CHECKING: - from .._core._tasks import CancelScope - -T_Retval = TypeVar("T_Retval") -T_contra = TypeVar("T_contra", contravariant=True) -PosArgsT = TypeVarTuple("PosArgsT") - - -class TaskStatus(Protocol[T_contra]): - @overload - def started(self: TaskStatus[None]) -> None: ... - - @overload - def started(self, value: T_contra) -> None: ... - - def started(self, value: T_contra | None = None) -> None: - """ - Signal that the task has started. - - :param value: object passed back to the starter of the task - """ - - -class TaskGroup(metaclass=ABCMeta): - """ - Groups several asynchronous tasks together. - - :ivar cancel_scope: the cancel scope inherited by all child tasks - :vartype cancel_scope: CancelScope - - .. note:: On asyncio, support for eager task factories is considered to be - **experimental**. In particular, they don't follow the usual semantics of new - tasks being scheduled on the next iteration of the event loop, and may thus - cause unexpected behavior in code that wasn't written with such semantics in - mind. - """ - - cancel_scope: CancelScope - - @abstractmethod - def start_soon( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], - *args: Unpack[PosArgsT], - name: object = None, - ) -> None: - """ - Start a new task in this task group. - - :param func: a coroutine function - :param args: positional arguments to call the function with - :param name: name of the task, for the purposes of introspection and debugging - - .. versionadded:: 3.0 - """ - - @abstractmethod - async def start( - self, - func: Callable[..., Awaitable[Any]], - *args: object, - name: object = None, - ) -> Any: - """ - Start a new task and wait until it signals for readiness. - - :param func: a coroutine function - :param args: positional arguments to call the function with - :param name: name of the task, for the purposes of introspection and debugging - :return: the value passed to ``task_status.started()`` - :raises RuntimeError: if the task finishes without calling - ``task_status.started()`` - - .. versionadded:: 3.0 - """ - - @abstractmethod - async def __aenter__(self) -> TaskGroup: - """Enter the task group context and allow starting new tasks.""" - - @abstractmethod - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the task group context waiting for all tasks to finish.""" diff --git a/venv/lib/python3.12/site-packages/anyio/abc/_testing.py b/venv/lib/python3.12/site-packages/anyio/abc/_testing.py deleted file mode 100644 index 7c50ed7..0000000 --- a/venv/lib/python3.12/site-packages/anyio/abc/_testing.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -import types -from abc import ABCMeta, abstractmethod -from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable -from typing import Any, TypeVar - -_T = TypeVar("_T") - - -class TestRunner(metaclass=ABCMeta): - """ - Encapsulates a running event loop. Every call made through this object will use the - same event loop. - """ - - def __enter__(self) -> TestRunner: - return self - - @abstractmethod - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: types.TracebackType | None, - ) -> bool | None: ... - - @abstractmethod - def run_asyncgen_fixture( - self, - fixture_func: Callable[..., AsyncGenerator[_T, Any]], - kwargs: dict[str, Any], - ) -> Iterable[_T]: - """ - Run an async generator fixture. - - :param fixture_func: the fixture function - :param kwargs: keyword arguments to call the fixture function with - :return: an iterator yielding the value yielded from the async generator - """ - - @abstractmethod - def run_fixture( - self, - fixture_func: Callable[..., Coroutine[Any, Any, _T]], - kwargs: dict[str, Any], - ) -> _T: - """ - Run an async fixture. - - :param fixture_func: the fixture function - :param kwargs: keyword arguments to call the fixture function with - :return: the return value of the fixture function - """ - - @abstractmethod - def run_test( - self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] - ) -> None: - """ - Run an async test function. - - :param test_func: the test function - :param kwargs: keyword arguments to call the test function with - """ diff --git a/venv/lib/python3.12/site-packages/anyio/from_thread.py b/venv/lib/python3.12/site-packages/anyio/from_thread.py deleted file mode 100644 index 6179097..0000000 --- a/venv/lib/python3.12/site-packages/anyio/from_thread.py +++ /dev/null @@ -1,527 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import Awaitable, Callable, Generator -from concurrent.futures import Future -from contextlib import ( - AbstractAsyncContextManager, - AbstractContextManager, - contextmanager, -) -from dataclasses import dataclass, field -from inspect import isawaitable -from threading import Lock, Thread, get_ident -from types import TracebackType -from typing import ( - Any, - Generic, - TypeVar, - cast, - overload, -) - -from ._core import _eventloop -from ._core._eventloop import get_async_backend, get_cancelled_exc_class, threadlocals -from ._core._synchronization import Event -from ._core._tasks import CancelScope, create_task_group -from .abc import AsyncBackend -from .abc._tasks import TaskStatus - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -T_Retval = TypeVar("T_Retval") -T_co = TypeVar("T_co", covariant=True) -PosArgsT = TypeVarTuple("PosArgsT") - - -def run( - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], *args: Unpack[PosArgsT] -) -> T_Retval: - """ - Call a coroutine function from a worker thread. - - :param func: a coroutine function - :param args: positional arguments for the callable - :return: the return value of the coroutine function - - """ - try: - async_backend = threadlocals.current_async_backend - token = threadlocals.current_token - except AttributeError: - raise RuntimeError( - "This function can only be run from an AnyIO worker thread" - ) from None - - return async_backend.run_async_from_thread(func, args, token=token) - - -def run_sync( - func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] -) -> T_Retval: - """ - Call a function in the event loop thread from a worker thread. - - :param func: a callable - :param args: positional arguments for the callable - :return: the return value of the callable - - """ - try: - async_backend = threadlocals.current_async_backend - token = threadlocals.current_token - except AttributeError: - raise RuntimeError( - "This function can only be run from an AnyIO worker thread" - ) from None - - return async_backend.run_sync_from_thread(func, args, token=token) - - -class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager): - _enter_future: Future[T_co] - _exit_future: Future[bool | None] - _exit_event: Event - _exit_exc_info: tuple[ - type[BaseException] | None, BaseException | None, TracebackType | None - ] = (None, None, None) - - def __init__( - self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal - ): - self._async_cm = async_cm - self._portal = portal - - async def run_async_cm(self) -> bool | None: - try: - self._exit_event = Event() - value = await self._async_cm.__aenter__() - except BaseException as exc: - self._enter_future.set_exception(exc) - raise - else: - self._enter_future.set_result(value) - - try: - # Wait for the sync context manager to exit. - # This next statement can raise `get_cancelled_exc_class()` if - # something went wrong in a task group in this async context - # manager. - await self._exit_event.wait() - finally: - # In case of cancellation, it could be that we end up here before - # `_BlockingAsyncContextManager.__exit__` is called, and an - # `_exit_exc_info` has been set. - result = await self._async_cm.__aexit__(*self._exit_exc_info) - return result - - def __enter__(self) -> T_co: - self._enter_future = Future() - self._exit_future = self._portal.start_task_soon(self.run_async_cm) - return self._enter_future.result() - - def __exit__( - self, - __exc_type: type[BaseException] | None, - __exc_value: BaseException | None, - __traceback: TracebackType | None, - ) -> bool | None: - self._exit_exc_info = __exc_type, __exc_value, __traceback - self._portal.call(self._exit_event.set) - return self._exit_future.result() - - -class _BlockingPortalTaskStatus(TaskStatus): - def __init__(self, future: Future): - self._future = future - - def started(self, value: object = None) -> None: - self._future.set_result(value) - - -class BlockingPortal: - """An object that lets external threads run code in an asynchronous event loop.""" - - def __new__(cls) -> BlockingPortal: - return get_async_backend().create_blocking_portal() - - def __init__(self) -> None: - self._event_loop_thread_id: int | None = get_ident() - self._stop_event = Event() - self._task_group = create_task_group() - self._cancelled_exc_class = get_cancelled_exc_class() - - async def __aenter__(self) -> BlockingPortal: - await self._task_group.__aenter__() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - await self.stop() - return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) - - def _check_running(self) -> None: - if self._event_loop_thread_id is None: - raise RuntimeError("This portal is not running") - if self._event_loop_thread_id == get_ident(): - raise RuntimeError( - "This method cannot be called from the event loop thread" - ) - - async def sleep_until_stopped(self) -> None: - """Sleep until :meth:`stop` is called.""" - await self._stop_event.wait() - - async def stop(self, cancel_remaining: bool = False) -> None: - """ - Signal the portal to shut down. - - This marks the portal as no longer accepting new calls and exits from - :meth:`sleep_until_stopped`. - - :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` - to let them finish before returning - - """ - self._event_loop_thread_id = None - self._stop_event.set() - if cancel_remaining: - self._task_group.cancel_scope.cancel() - - async def _call_func( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - future: Future[T_Retval], - ) -> None: - def callback(f: Future[T_Retval]) -> None: - if f.cancelled() and self._event_loop_thread_id not in ( - None, - get_ident(), - ): - self.call(scope.cancel) - - try: - retval_or_awaitable = func(*args, **kwargs) - if isawaitable(retval_or_awaitable): - with CancelScope() as scope: - if future.cancelled(): - scope.cancel() - else: - future.add_done_callback(callback) - - retval = await retval_or_awaitable - else: - retval = retval_or_awaitable - except self._cancelled_exc_class: - future.cancel() - future.set_running_or_notify_cancel() - except BaseException as exc: - if not future.cancelled(): - future.set_exception(exc) - - # Let base exceptions fall through - if not isinstance(exc, Exception): - raise - else: - if not future.cancelled(): - future.set_result(retval) - finally: - scope = None # type: ignore[assignment] - - def _spawn_task_from_thread( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - args: tuple[Unpack[PosArgsT]], - kwargs: dict[str, Any], - name: object, - future: Future[T_Retval], - ) -> None: - """ - Spawn a new task using the given callable. - - Implementers must ensure that the future is resolved when the task finishes. - - :param func: a callable - :param args: positional arguments to be passed to the callable - :param kwargs: keyword arguments to be passed to the callable - :param name: name of the task (will be coerced to a string if not ``None``) - :param future: a future that will resolve to the return value of the callable, - or the exception raised during its execution - - """ - raise NotImplementedError - - @overload - def call( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - *args: Unpack[PosArgsT], - ) -> T_Retval: ... - - @overload - def call( - self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] - ) -> T_Retval: ... - - def call( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - *args: Unpack[PosArgsT], - ) -> T_Retval: - """ - Call the given function in the event loop thread. - - If the callable returns a coroutine object, it is awaited on. - - :param func: any callable - :raises RuntimeError: if the portal is not running or if this method is called - from within the event loop thread - - """ - return cast(T_Retval, self.start_task_soon(func, *args).result()) - - @overload - def start_task_soon( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], - *args: Unpack[PosArgsT], - name: object = None, - ) -> Future[T_Retval]: ... - - @overload - def start_task_soon( - self, - func: Callable[[Unpack[PosArgsT]], T_Retval], - *args: Unpack[PosArgsT], - name: object = None, - ) -> Future[T_Retval]: ... - - def start_task_soon( - self, - func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], - *args: Unpack[PosArgsT], - name: object = None, - ) -> Future[T_Retval]: - """ - Start a task in the portal's task group. - - The task will be run inside a cancel scope which can be cancelled by cancelling - the returned future. - - :param func: the target function - :param args: positional arguments passed to ``func`` - :param name: name of the task (will be coerced to a string if not ``None``) - :return: a future that resolves with the return value of the callable if the - task completes successfully, or with the exception raised in the task - :raises RuntimeError: if the portal is not running or if this method is called - from within the event loop thread - :rtype: concurrent.futures.Future[T_Retval] - - .. versionadded:: 3.0 - - """ - self._check_running() - f: Future[T_Retval] = Future() - self._spawn_task_from_thread(func, args, {}, name, f) - return f - - def start_task( - self, - func: Callable[..., Awaitable[T_Retval]], - *args: object, - name: object = None, - ) -> tuple[Future[T_Retval], Any]: - """ - Start a task in the portal's task group and wait until it signals for readiness. - - This method works the same way as :meth:`.abc.TaskGroup.start`. - - :param func: the target function - :param args: positional arguments passed to ``func`` - :param name: name of the task (will be coerced to a string if not ``None``) - :return: a tuple of (future, task_status_value) where the ``task_status_value`` - is the value passed to ``task_status.started()`` from within the target - function - :rtype: tuple[concurrent.futures.Future[T_Retval], Any] - - .. versionadded:: 3.0 - - """ - - def task_done(future: Future[T_Retval]) -> None: - if not task_status_future.done(): - if future.cancelled(): - task_status_future.cancel() - elif future.exception(): - task_status_future.set_exception(future.exception()) - else: - exc = RuntimeError( - "Task exited without calling task_status.started()" - ) - task_status_future.set_exception(exc) - - self._check_running() - task_status_future: Future = Future() - task_status = _BlockingPortalTaskStatus(task_status_future) - f: Future = Future() - f.add_done_callback(task_done) - self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) - return f, task_status_future.result() - - def wrap_async_context_manager( - self, cm: AbstractAsyncContextManager[T_co] - ) -> AbstractContextManager[T_co]: - """ - Wrap an async context manager as a synchronous context manager via this portal. - - Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping - in the middle until the synchronous context manager exits. - - :param cm: an asynchronous context manager - :return: a synchronous context manager - - .. versionadded:: 2.1 - - """ - return _BlockingAsyncContextManager(cm, self) - - -@dataclass -class BlockingPortalProvider: - """ - A manager for a blocking portal. Used as a context manager. The first thread to - enter this context manager causes a blocking portal to be started with the specific - parameters, and the last thread to exit causes the portal to be shut down. Thus, - there will be exactly one blocking portal running in this context as long as at - least one thread has entered this context manager. - - The parameters are the same as for :func:`~anyio.run`. - - :param backend: name of the backend - :param backend_options: backend options - - .. versionadded:: 4.4 - """ - - backend: str = "asyncio" - backend_options: dict[str, Any] | None = None - _lock: Lock = field(init=False, default_factory=Lock) - _leases: int = field(init=False, default=0) - _portal: BlockingPortal = field(init=False) - _portal_cm: AbstractContextManager[BlockingPortal] | None = field( - init=False, default=None - ) - - def __enter__(self) -> BlockingPortal: - with self._lock: - if self._portal_cm is None: - self._portal_cm = start_blocking_portal( - self.backend, self.backend_options - ) - self._portal = self._portal_cm.__enter__() - - self._leases += 1 - return self._portal - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - portal_cm: AbstractContextManager[BlockingPortal] | None = None - with self._lock: - assert self._portal_cm - assert self._leases > 0 - self._leases -= 1 - if not self._leases: - portal_cm = self._portal_cm - self._portal_cm = None - del self._portal - - if portal_cm: - portal_cm.__exit__(None, None, None) - - -@contextmanager -def start_blocking_portal( - backend: str = "asyncio", backend_options: dict[str, Any] | None = None -) -> Generator[BlockingPortal, Any, None]: - """ - Start a new event loop in a new thread and run a blocking portal in its main task. - - The parameters are the same as for :func:`~anyio.run`. - - :param backend: name of the backend - :param backend_options: backend options - :return: a context manager that yields a blocking portal - - .. versionchanged:: 3.0 - Usage as a context manager is now required. - - """ - - async def run_portal() -> None: - async with BlockingPortal() as portal_: - future.set_result(portal_) - await portal_.sleep_until_stopped() - - def run_blocking_portal() -> None: - if future.set_running_or_notify_cancel(): - try: - _eventloop.run( - run_portal, backend=backend, backend_options=backend_options - ) - except BaseException as exc: - if not future.done(): - future.set_exception(exc) - - future: Future[BlockingPortal] = Future() - thread = Thread(target=run_blocking_portal, daemon=True) - thread.start() - try: - cancel_remaining_tasks = False - portal = future.result() - try: - yield portal - except BaseException: - cancel_remaining_tasks = True - raise - finally: - try: - portal.call(portal.stop, cancel_remaining_tasks) - except RuntimeError: - pass - finally: - thread.join() - - -def check_cancelled() -> None: - """ - Check if the cancel scope of the host task's running the current worker thread has - been cancelled. - - If the host task's current cancel scope has indeed been cancelled, the - backend-specific cancellation exception will be raised. - - :raises RuntimeError: if the current thread was not spawned by - :func:`.to_thread.run_sync` - - """ - try: - async_backend: AsyncBackend = threadlocals.current_async_backend - except AttributeError: - raise RuntimeError( - "This function can only be run from an AnyIO worker thread" - ) from None - - async_backend.check_cancelled() diff --git a/venv/lib/python3.12/site-packages/anyio/lowlevel.py b/venv/lib/python3.12/site-packages/anyio/lowlevel.py deleted file mode 100644 index 14c7668..0000000 --- a/venv/lib/python3.12/site-packages/anyio/lowlevel.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -import enum -from dataclasses import dataclass -from typing import Any, Generic, Literal, TypeVar, overload -from weakref import WeakKeyDictionary - -from ._core._eventloop import get_async_backend - -T = TypeVar("T") -D = TypeVar("D") - - -async def checkpoint() -> None: - """ - Check for cancellation and allow the scheduler to switch to another task. - - Equivalent to (but more efficient than):: - - await checkpoint_if_cancelled() - await cancel_shielded_checkpoint() - - - .. versionadded:: 3.0 - - """ - await get_async_backend().checkpoint() - - -async def checkpoint_if_cancelled() -> None: - """ - Enter a checkpoint if the enclosing cancel scope has been cancelled. - - This does not allow the scheduler to switch to a different task. - - .. versionadded:: 3.0 - - """ - await get_async_backend().checkpoint_if_cancelled() - - -async def cancel_shielded_checkpoint() -> None: - """ - Allow the scheduler to switch to another task but without checking for cancellation. - - Equivalent to (but potentially more efficient than):: - - with CancelScope(shield=True): - await checkpoint() - - - .. versionadded:: 3.0 - - """ - await get_async_backend().cancel_shielded_checkpoint() - - -def current_token() -> object: - """ - Return a backend specific token object that can be used to get back to the event - loop. - - """ - return get_async_backend().current_token() - - -_run_vars: WeakKeyDictionary[Any, dict[str, Any]] = WeakKeyDictionary() -_token_wrappers: dict[Any, _TokenWrapper] = {} - - -@dataclass(frozen=True) -class _TokenWrapper: - __slots__ = "_token", "__weakref__" - _token: object - - -class _NoValueSet(enum.Enum): - NO_VALUE_SET = enum.auto() - - -class RunvarToken(Generic[T]): - __slots__ = "_var", "_value", "_redeemed" - - def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]): - self._var = var - self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value - self._redeemed = False - - -class RunVar(Generic[T]): - """ - Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop. - """ - - __slots__ = "_name", "_default" - - NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET - - _token_wrappers: set[_TokenWrapper] = set() - - def __init__( - self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET - ): - self._name = name - self._default = default - - @property - def _current_vars(self) -> dict[str, T]: - token = current_token() - try: - return _run_vars[token] - except KeyError: - run_vars = _run_vars[token] = {} - return run_vars - - @overload - def get(self, default: D) -> T | D: ... - - @overload - def get(self) -> T: ... - - def get( - self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET - ) -> T | D: - try: - return self._current_vars[self._name] - except KeyError: - if default is not RunVar.NO_VALUE_SET: - return default - elif self._default is not RunVar.NO_VALUE_SET: - return self._default - - raise LookupError( - f'Run variable "{self._name}" has no value and no default set' - ) - - def set(self, value: T) -> RunvarToken[T]: - current_vars = self._current_vars - token = RunvarToken(self, current_vars.get(self._name, RunVar.NO_VALUE_SET)) - current_vars[self._name] = value - return token - - def reset(self, token: RunvarToken[T]) -> None: - if token._var is not self: - raise ValueError("This token does not belong to this RunVar") - - if token._redeemed: - raise ValueError("This token has already been used") - - if token._value is _NoValueSet.NO_VALUE_SET: - try: - del self._current_vars[self._name] - except KeyError: - pass - else: - self._current_vars[self._name] = token._value - - token._redeemed = True - - def __repr__(self) -> str: - return f"" diff --git a/venv/lib/python3.12/site-packages/anyio/py.typed b/venv/lib/python3.12/site-packages/anyio/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py b/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py deleted file mode 100644 index 21e4ab2..0000000 --- a/venv/lib/python3.12/site-packages/anyio/pytest_plugin.py +++ /dev/null @@ -1,272 +0,0 @@ -from __future__ import annotations - -import socket -import sys -from collections.abc import Callable, Generator, Iterator -from contextlib import ExitStack, contextmanager -from inspect import isasyncgenfunction, iscoroutinefunction, ismethod -from typing import Any, cast - -import pytest -import sniffio -from _pytest.fixtures import SubRequest -from _pytest.outcomes import Exit - -from ._core._eventloop import get_all_backends, get_async_backend -from ._core._exceptions import iterate_exceptions -from .abc import TestRunner - -if sys.version_info < (3, 11): - from exceptiongroup import ExceptionGroup - -_current_runner: TestRunner | None = None -_runner_stack: ExitStack | None = None -_runner_leases = 0 - - -def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]: - if isinstance(backend, str): - return backend, {} - elif isinstance(backend, tuple) and len(backend) == 2: - if isinstance(backend[0], str) and isinstance(backend[1], dict): - return cast(tuple[str, dict[str, Any]], backend) - - raise TypeError("anyio_backend must be either a string or tuple of (string, dict)") - - -@contextmanager -def get_runner( - backend_name: str, backend_options: dict[str, Any] -) -> Iterator[TestRunner]: - global _current_runner, _runner_leases, _runner_stack - if _current_runner is None: - asynclib = get_async_backend(backend_name) - _runner_stack = ExitStack() - if sniffio.current_async_library_cvar.get(None) is None: - # Since we're in control of the event loop, we can cache the name of the - # async library - token = sniffio.current_async_library_cvar.set(backend_name) - _runner_stack.callback(sniffio.current_async_library_cvar.reset, token) - - backend_options = backend_options or {} - _current_runner = _runner_stack.enter_context( - asynclib.create_test_runner(backend_options) - ) - - _runner_leases += 1 - try: - yield _current_runner - finally: - _runner_leases -= 1 - if not _runner_leases: - assert _runner_stack is not None - _runner_stack.close() - _runner_stack = _current_runner = None - - -def pytest_configure(config: Any) -> None: - config.addinivalue_line( - "markers", - "anyio: mark the (coroutine function) test to be run asynchronously via anyio.", - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]: - def wrapper( - *args: Any, anyio_backend: Any, request: SubRequest, **kwargs: Any - ) -> Any: - # Rebind any fixture methods to the request instance - if ( - request.instance - and ismethod(func) - and type(func.__self__) is type(request.instance) - ): - local_func = func.__func__.__get__(request.instance) - else: - local_func = func - - backend_name, backend_options = extract_backend_and_options(anyio_backend) - if has_backend_arg: - kwargs["anyio_backend"] = anyio_backend - - if has_request_arg: - kwargs["request"] = request - - with get_runner(backend_name, backend_options) as runner: - if isasyncgenfunction(local_func): - yield from runner.run_asyncgen_fixture(local_func, kwargs) - else: - yield runner.run_fixture(local_func, kwargs) - - # Only apply this to coroutine functions and async generator functions in requests - # that involve the anyio_backend fixture - func = fixturedef.func - if isasyncgenfunction(func) or iscoroutinefunction(func): - if "anyio_backend" in request.fixturenames: - fixturedef.func = wrapper - original_argname = fixturedef.argnames - - if not (has_backend_arg := "anyio_backend" in fixturedef.argnames): - fixturedef.argnames += ("anyio_backend",) - - if not (has_request_arg := "request" in fixturedef.argnames): - fixturedef.argnames += ("request",) - - try: - return (yield) - finally: - fixturedef.func = func - fixturedef.argnames = original_argname - - return (yield) - - -@pytest.hookimpl(tryfirst=True) -def pytest_pycollect_makeitem(collector: Any, name: Any, obj: Any) -> None: - if collector.istestfunction(obj, name): - inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj - if iscoroutinefunction(inner_func): - marker = collector.get_closest_marker("anyio") - own_markers = getattr(obj, "pytestmark", ()) - if marker or any(marker.name == "anyio" for marker in own_markers): - pytest.mark.usefixtures("anyio_backend")(obj) - - -@pytest.hookimpl(tryfirst=True) -def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None: - def run_with_hypothesis(**kwargs: Any) -> None: - with get_runner(backend_name, backend_options) as runner: - runner.run_test(original_func, kwargs) - - backend = pyfuncitem.funcargs.get("anyio_backend") - if backend: - backend_name, backend_options = extract_backend_and_options(backend) - - if hasattr(pyfuncitem.obj, "hypothesis"): - # Wrap the inner test function unless it's already wrapped - original_func = pyfuncitem.obj.hypothesis.inner_test - if original_func.__qualname__ != run_with_hypothesis.__qualname__: - if iscoroutinefunction(original_func): - pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis - - return None - - if iscoroutinefunction(pyfuncitem.obj): - funcargs = pyfuncitem.funcargs - testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} - with get_runner(backend_name, backend_options) as runner: - try: - runner.run_test(pyfuncitem.obj, testargs) - except ExceptionGroup as excgrp: - for exc in iterate_exceptions(excgrp): - if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)): - raise exc from excgrp - - raise - - return True - - return None - - -@pytest.fixture(scope="module", params=get_all_backends()) -def anyio_backend(request: Any) -> Any: - return request.param - - -@pytest.fixture -def anyio_backend_name(anyio_backend: Any) -> str: - if isinstance(anyio_backend, str): - return anyio_backend - else: - return anyio_backend[0] - - -@pytest.fixture -def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]: - if isinstance(anyio_backend, str): - return {} - else: - return anyio_backend[1] - - -class FreePortFactory: - """ - Manages port generation based on specified socket kind, ensuring no duplicate - ports are generated. - - This class provides functionality for generating available free ports on the - system. It is initialized with a specific socket kind and can generate ports - for given address families while avoiding reuse of previously generated ports. - - Users should not instantiate this class directly, but use the - ``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple - uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead. - """ - - def __init__(self, kind: socket.SocketKind) -> None: - self._kind = kind - self._generated = set[int]() - - @property - def kind(self) -> socket.SocketKind: - """ - The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or - :data:`~socket.SOCK_DGRAM`) used to bind for checking port availability - - """ - return self._kind - - def __call__(self, family: socket.AddressFamily | None = None) -> int: - """ - Return an unbound port for the given address family. - - :param family: if omitted, both IPv4 and IPv6 addresses will be tried - :return: a port number - - """ - if family is not None: - families = [family] - else: - families = [socket.AF_INET] - if socket.has_ipv6: - families.append(socket.AF_INET6) - - while True: - port = 0 - with ExitStack() as stack: - for family in families: - sock = stack.enter_context(socket.socket(family, self._kind)) - addr = "::1" if family == socket.AF_INET6 else "127.0.0.1" - try: - sock.bind((addr, port)) - except OSError: - break - - if not port: - port = sock.getsockname()[1] - else: - if port not in self._generated: - self._generated.add(port) - return port - - -@pytest.fixture(scope="session") -def free_tcp_port_factory() -> FreePortFactory: - return FreePortFactory(socket.SOCK_STREAM) - - -@pytest.fixture(scope="session") -def free_udp_port_factory() -> FreePortFactory: - return FreePortFactory(socket.SOCK_DGRAM) - - -@pytest.fixture -def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int: - return free_tcp_port_factory() - - -@pytest.fixture -def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int: - return free_udp_port_factory() diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__init__.py b/venv/lib/python3.12/site-packages/anyio/streams/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index edce2b4..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc deleted file mode 100644 index e7dea36..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/buffered.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc deleted file mode 100644 index fab4dcf..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/file.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc deleted file mode 100644 index 0596eda..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/memory.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc deleted file mode 100644 index 4ceba34..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/stapled.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc deleted file mode 100644 index ace62d4..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/text.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc b/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc deleted file mode 100644 index 439ed55..0000000 Binary files a/venv/lib/python3.12/site-packages/anyio/streams/__pycache__/tls.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/anyio/streams/buffered.py b/venv/lib/python3.12/site-packages/anyio/streams/buffered.py deleted file mode 100644 index f5d5e83..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/buffered.py +++ /dev/null @@ -1,119 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Mapping -from dataclasses import dataclass, field -from typing import Any - -from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead -from ..abc import AnyByteReceiveStream, ByteReceiveStream - - -@dataclass(eq=False) -class BufferedByteReceiveStream(ByteReceiveStream): - """ - Wraps any bytes-based receive stream and uses a buffer to provide sophisticated - receiving capabilities in the form of a byte stream. - """ - - receive_stream: AnyByteReceiveStream - _buffer: bytearray = field(init=False, default_factory=bytearray) - _closed: bool = field(init=False, default=False) - - async def aclose(self) -> None: - await self.receive_stream.aclose() - self._closed = True - - @property - def buffer(self) -> bytes: - """The bytes currently in the buffer.""" - return bytes(self._buffer) - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return self.receive_stream.extra_attributes - - async def receive(self, max_bytes: int = 65536) -> bytes: - if self._closed: - raise ClosedResourceError - - if self._buffer: - chunk = bytes(self._buffer[:max_bytes]) - del self._buffer[:max_bytes] - return chunk - elif isinstance(self.receive_stream, ByteReceiveStream): - return await self.receive_stream.receive(max_bytes) - else: - # With a bytes-oriented object stream, we need to handle any surplus bytes - # we get from the receive() call - chunk = await self.receive_stream.receive() - if len(chunk) > max_bytes: - # Save the surplus bytes in the buffer - self._buffer.extend(chunk[max_bytes:]) - return chunk[:max_bytes] - else: - return chunk - - async def receive_exactly(self, nbytes: int) -> bytes: - """ - Read exactly the given amount of bytes from the stream. - - :param nbytes: the number of bytes to read - :return: the bytes read - :raises ~anyio.IncompleteRead: if the stream was closed before the requested - amount of bytes could be read from the stream - - """ - while True: - remaining = nbytes - len(self._buffer) - if remaining <= 0: - retval = self._buffer[:nbytes] - del self._buffer[:nbytes] - return bytes(retval) - - try: - if isinstance(self.receive_stream, ByteReceiveStream): - chunk = await self.receive_stream.receive(remaining) - else: - chunk = await self.receive_stream.receive() - except EndOfStream as exc: - raise IncompleteRead from exc - - self._buffer.extend(chunk) - - async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes: - """ - Read from the stream until the delimiter is found or max_bytes have been read. - - :param delimiter: the marker to look for in the stream - :param max_bytes: maximum number of bytes that will be read before raising - :exc:`~anyio.DelimiterNotFound` - :return: the bytes read (not including the delimiter) - :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter - was found - :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the - bytes read up to the maximum allowed - - """ - delimiter_size = len(delimiter) - offset = 0 - while True: - # Check if the delimiter can be found in the current buffer - index = self._buffer.find(delimiter, offset) - if index >= 0: - found = self._buffer[:index] - del self._buffer[: index + len(delimiter) :] - return bytes(found) - - # Check if the buffer is already at or over the limit - if len(self._buffer) >= max_bytes: - raise DelimiterNotFound(max_bytes) - - # Read more data into the buffer from the socket - try: - data = await self.receive_stream.receive() - except EndOfStream as exc: - raise IncompleteRead from exc - - # Move the offset forward and add the new data to the buffer - offset = max(len(self._buffer) - delimiter_size + 1, 0) - self._buffer.extend(data) diff --git a/venv/lib/python3.12/site-packages/anyio/streams/file.py b/venv/lib/python3.12/site-packages/anyio/streams/file.py deleted file mode 100644 index f492464..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/file.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Mapping -from io import SEEK_SET, UnsupportedOperation -from os import PathLike -from pathlib import Path -from typing import Any, BinaryIO, cast - -from .. import ( - BrokenResourceError, - ClosedResourceError, - EndOfStream, - TypedAttributeSet, - to_thread, - typed_attribute, -) -from ..abc import ByteReceiveStream, ByteSendStream - - -class FileStreamAttribute(TypedAttributeSet): - #: the open file descriptor - file: BinaryIO = typed_attribute() - #: the path of the file on the file system, if available (file must be a real file) - path: Path = typed_attribute() - #: the file number, if available (file must be a real file or a TTY) - fileno: int = typed_attribute() - - -class _BaseFileStream: - def __init__(self, file: BinaryIO): - self._file = file - - async def aclose(self) -> None: - await to_thread.run_sync(self._file.close) - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - attributes: dict[Any, Callable[[], Any]] = { - FileStreamAttribute.file: lambda: self._file, - } - - if hasattr(self._file, "name"): - attributes[FileStreamAttribute.path] = lambda: Path(self._file.name) - - try: - self._file.fileno() - except UnsupportedOperation: - pass - else: - attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno() - - return attributes - - -class FileReadStream(_BaseFileStream, ByteReceiveStream): - """ - A byte stream that reads from a file in the file system. - - :param file: a file that has been opened for reading in binary mode - - .. versionadded:: 3.0 - """ - - @classmethod - async def from_path(cls, path: str | PathLike[str]) -> FileReadStream: - """ - Create a file read stream by opening the given file. - - :param path: path of the file to read from - - """ - file = await to_thread.run_sync(Path(path).open, "rb") - return cls(cast(BinaryIO, file)) - - async def receive(self, max_bytes: int = 65536) -> bytes: - try: - data = await to_thread.run_sync(self._file.read, max_bytes) - except ValueError: - raise ClosedResourceError from None - except OSError as exc: - raise BrokenResourceError from exc - - if data: - return data - else: - raise EndOfStream - - async def seek(self, position: int, whence: int = SEEK_SET) -> int: - """ - Seek the file to the given position. - - .. seealso:: :meth:`io.IOBase.seek` - - .. note:: Not all file descriptors are seekable. - - :param position: position to seek the file to - :param whence: controls how ``position`` is interpreted - :return: the new absolute position - :raises OSError: if the file is not seekable - - """ - return await to_thread.run_sync(self._file.seek, position, whence) - - async def tell(self) -> int: - """ - Return the current stream position. - - .. note:: Not all file descriptors are seekable. - - :return: the current absolute position - :raises OSError: if the file is not seekable - - """ - return await to_thread.run_sync(self._file.tell) - - -class FileWriteStream(_BaseFileStream, ByteSendStream): - """ - A byte stream that writes to a file in the file system. - - :param file: a file that has been opened for writing in binary mode - - .. versionadded:: 3.0 - """ - - @classmethod - async def from_path( - cls, path: str | PathLike[str], append: bool = False - ) -> FileWriteStream: - """ - Create a file write stream by opening the given file for writing. - - :param path: path of the file to write to - :param append: if ``True``, open the file for appending; if ``False``, any - existing file at the given path will be truncated - - """ - mode = "ab" if append else "wb" - file = await to_thread.run_sync(Path(path).open, mode) - return cls(cast(BinaryIO, file)) - - async def send(self, item: bytes) -> None: - try: - await to_thread.run_sync(self._file.write, item) - except ValueError: - raise ClosedResourceError from None - except OSError as exc: - raise BrokenResourceError from exc diff --git a/venv/lib/python3.12/site-packages/anyio/streams/memory.py b/venv/lib/python3.12/site-packages/anyio/streams/memory.py deleted file mode 100644 index 83bf1d9..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/memory.py +++ /dev/null @@ -1,317 +0,0 @@ -from __future__ import annotations - -import warnings -from collections import OrderedDict, deque -from dataclasses import dataclass, field -from types import TracebackType -from typing import Generic, NamedTuple, TypeVar - -from .. import ( - BrokenResourceError, - ClosedResourceError, - EndOfStream, - WouldBlock, -) -from .._core._testing import TaskInfo, get_current_task -from ..abc import Event, ObjectReceiveStream, ObjectSendStream -from ..lowlevel import checkpoint - -T_Item = TypeVar("T_Item") -T_co = TypeVar("T_co", covariant=True) -T_contra = TypeVar("T_contra", contravariant=True) - - -class MemoryObjectStreamStatistics(NamedTuple): - current_buffer_used: int #: number of items stored in the buffer - #: maximum number of items that can be stored on this stream (or :data:`math.inf`) - max_buffer_size: float - open_send_streams: int #: number of unclosed clones of the send stream - open_receive_streams: int #: number of unclosed clones of the receive stream - #: number of tasks blocked on :meth:`MemoryObjectSendStream.send` - tasks_waiting_send: int - #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive` - tasks_waiting_receive: int - - -@dataclass(eq=False) -class MemoryObjectItemReceiver(Generic[T_Item]): - task_info: TaskInfo = field(init=False, default_factory=get_current_task) - item: T_Item = field(init=False) - - def __repr__(self) -> str: - # When item is not defined, we get following error with default __repr__: - # AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item' - item = getattr(self, "item", None) - return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})" - - -@dataclass(eq=False) -class MemoryObjectStreamState(Generic[T_Item]): - max_buffer_size: float = field() - buffer: deque[T_Item] = field(init=False, default_factory=deque) - open_send_channels: int = field(init=False, default=0) - open_receive_channels: int = field(init=False, default=0) - waiting_receivers: OrderedDict[Event, MemoryObjectItemReceiver[T_Item]] = field( - init=False, default_factory=OrderedDict - ) - waiting_senders: OrderedDict[Event, T_Item] = field( - init=False, default_factory=OrderedDict - ) - - def statistics(self) -> MemoryObjectStreamStatistics: - return MemoryObjectStreamStatistics( - len(self.buffer), - self.max_buffer_size, - self.open_send_channels, - self.open_receive_channels, - len(self.waiting_senders), - len(self.waiting_receivers), - ) - - -@dataclass(eq=False) -class MemoryObjectReceiveStream(Generic[T_co], ObjectReceiveStream[T_co]): - _state: MemoryObjectStreamState[T_co] - _closed: bool = field(init=False, default=False) - - def __post_init__(self) -> None: - self._state.open_receive_channels += 1 - - def receive_nowait(self) -> T_co: - """ - Receive the next item if it can be done without waiting. - - :return: the received item - :raises ~anyio.ClosedResourceError: if this send stream has been closed - :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been - closed from the sending end - :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks - waiting to send - - """ - if self._closed: - raise ClosedResourceError - - if self._state.waiting_senders: - # Get the item from the next sender - send_event, item = self._state.waiting_senders.popitem(last=False) - self._state.buffer.append(item) - send_event.set() - - if self._state.buffer: - return self._state.buffer.popleft() - elif not self._state.open_send_channels: - raise EndOfStream - - raise WouldBlock - - async def receive(self) -> T_co: - await checkpoint() - try: - return self.receive_nowait() - except WouldBlock: - # Add ourselves in the queue - receive_event = Event() - receiver = MemoryObjectItemReceiver[T_co]() - self._state.waiting_receivers[receive_event] = receiver - - try: - await receive_event.wait() - finally: - self._state.waiting_receivers.pop(receive_event, None) - - try: - return receiver.item - except AttributeError: - raise EndOfStream from None - - def clone(self) -> MemoryObjectReceiveStream[T_co]: - """ - Create a clone of this receive stream. - - Each clone can be closed separately. Only when all clones have been closed will - the receiving end of the memory stream be considered closed by the sending ends. - - :return: the cloned stream - - """ - if self._closed: - raise ClosedResourceError - - return MemoryObjectReceiveStream(_state=self._state) - - def close(self) -> None: - """ - Close the stream. - - This works the exact same way as :meth:`aclose`, but is provided as a special - case for the benefit of synchronous callbacks. - - """ - if not self._closed: - self._closed = True - self._state.open_receive_channels -= 1 - if self._state.open_receive_channels == 0: - send_events = list(self._state.waiting_senders.keys()) - for event in send_events: - event.set() - - async def aclose(self) -> None: - self.close() - - def statistics(self) -> MemoryObjectStreamStatistics: - """ - Return statistics about the current state of this stream. - - .. versionadded:: 3.0 - """ - return self._state.statistics() - - def __enter__(self) -> MemoryObjectReceiveStream[T_co]: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def __del__(self) -> None: - if not self._closed: - warnings.warn( - f"Unclosed <{self.__class__.__name__} at {id(self):x}>", - ResourceWarning, - source=self, - ) - - -@dataclass(eq=False) -class MemoryObjectSendStream(Generic[T_contra], ObjectSendStream[T_contra]): - _state: MemoryObjectStreamState[T_contra] - _closed: bool = field(init=False, default=False) - - def __post_init__(self) -> None: - self._state.open_send_channels += 1 - - def send_nowait(self, item: T_contra) -> None: - """ - Send an item immediately if it can be done without waiting. - - :param item: the item to send - :raises ~anyio.ClosedResourceError: if this send stream has been closed - :raises ~anyio.BrokenResourceError: if the stream has been closed from the - receiving end - :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting - to receive - - """ - if self._closed: - raise ClosedResourceError - if not self._state.open_receive_channels: - raise BrokenResourceError - - while self._state.waiting_receivers: - receive_event, receiver = self._state.waiting_receivers.popitem(last=False) - if not receiver.task_info.has_pending_cancellation(): - receiver.item = item - receive_event.set() - return - - if len(self._state.buffer) < self._state.max_buffer_size: - self._state.buffer.append(item) - else: - raise WouldBlock - - async def send(self, item: T_contra) -> None: - """ - Send an item to the stream. - - If the buffer is full, this method blocks until there is again room in the - buffer or the item can be sent directly to a receiver. - - :param item: the item to send - :raises ~anyio.ClosedResourceError: if this send stream has been closed - :raises ~anyio.BrokenResourceError: if the stream has been closed from the - receiving end - - """ - await checkpoint() - try: - self.send_nowait(item) - except WouldBlock: - # Wait until there's someone on the receiving end - send_event = Event() - self._state.waiting_senders[send_event] = item - try: - await send_event.wait() - except BaseException: - self._state.waiting_senders.pop(send_event, None) - raise - - if send_event in self._state.waiting_senders: - del self._state.waiting_senders[send_event] - raise BrokenResourceError from None - - def clone(self) -> MemoryObjectSendStream[T_contra]: - """ - Create a clone of this send stream. - - Each clone can be closed separately. Only when all clones have been closed will - the sending end of the memory stream be considered closed by the receiving ends. - - :return: the cloned stream - - """ - if self._closed: - raise ClosedResourceError - - return MemoryObjectSendStream(_state=self._state) - - def close(self) -> None: - """ - Close the stream. - - This works the exact same way as :meth:`aclose`, but is provided as a special - case for the benefit of synchronous callbacks. - - """ - if not self._closed: - self._closed = True - self._state.open_send_channels -= 1 - if self._state.open_send_channels == 0: - receive_events = list(self._state.waiting_receivers.keys()) - self._state.waiting_receivers.clear() - for event in receive_events: - event.set() - - async def aclose(self) -> None: - self.close() - - def statistics(self) -> MemoryObjectStreamStatistics: - """ - Return statistics about the current state of this stream. - - .. versionadded:: 3.0 - """ - return self._state.statistics() - - def __enter__(self) -> MemoryObjectSendStream[T_contra]: - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() - - def __del__(self) -> None: - if not self._closed: - warnings.warn( - f"Unclosed <{self.__class__.__name__} at {id(self):x}>", - ResourceWarning, - source=self, - ) diff --git a/venv/lib/python3.12/site-packages/anyio/streams/stapled.py b/venv/lib/python3.12/site-packages/anyio/streams/stapled.py deleted file mode 100644 index 80f64a2..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/stapled.py +++ /dev/null @@ -1,141 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable, Mapping, Sequence -from dataclasses import dataclass -from typing import Any, Generic, TypeVar - -from ..abc import ( - ByteReceiveStream, - ByteSendStream, - ByteStream, - Listener, - ObjectReceiveStream, - ObjectSendStream, - ObjectStream, - TaskGroup, -) - -T_Item = TypeVar("T_Item") -T_Stream = TypeVar("T_Stream") - - -@dataclass(eq=False) -class StapledByteStream(ByteStream): - """ - Combines two byte streams into a single, bidirectional byte stream. - - Extra attributes will be provided from both streams, with the receive stream - providing the values in case of a conflict. - - :param ByteSendStream send_stream: the sending byte stream - :param ByteReceiveStream receive_stream: the receiving byte stream - """ - - send_stream: ByteSendStream - receive_stream: ByteReceiveStream - - async def receive(self, max_bytes: int = 65536) -> bytes: - return await self.receive_stream.receive(max_bytes) - - async def send(self, item: bytes) -> None: - await self.send_stream.send(item) - - async def send_eof(self) -> None: - await self.send_stream.aclose() - - async def aclose(self) -> None: - await self.send_stream.aclose() - await self.receive_stream.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return { - **self.send_stream.extra_attributes, - **self.receive_stream.extra_attributes, - } - - -@dataclass(eq=False) -class StapledObjectStream(Generic[T_Item], ObjectStream[T_Item]): - """ - Combines two object streams into a single, bidirectional object stream. - - Extra attributes will be provided from both streams, with the receive stream - providing the values in case of a conflict. - - :param ObjectSendStream send_stream: the sending object stream - :param ObjectReceiveStream receive_stream: the receiving object stream - """ - - send_stream: ObjectSendStream[T_Item] - receive_stream: ObjectReceiveStream[T_Item] - - async def receive(self) -> T_Item: - return await self.receive_stream.receive() - - async def send(self, item: T_Item) -> None: - await self.send_stream.send(item) - - async def send_eof(self) -> None: - await self.send_stream.aclose() - - async def aclose(self) -> None: - await self.send_stream.aclose() - await self.receive_stream.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return { - **self.send_stream.extra_attributes, - **self.receive_stream.extra_attributes, - } - - -@dataclass(eq=False) -class MultiListener(Generic[T_Stream], Listener[T_Stream]): - """ - Combines multiple listeners into one, serving connections from all of them at once. - - Any MultiListeners in the given collection of listeners will have their listeners - moved into this one. - - Extra attributes are provided from each listener, with each successive listener - overriding any conflicting attributes from the previous one. - - :param listeners: listeners to serve - :type listeners: Sequence[Listener[T_Stream]] - """ - - listeners: Sequence[Listener[T_Stream]] - - def __post_init__(self) -> None: - listeners: list[Listener[T_Stream]] = [] - for listener in self.listeners: - if isinstance(listener, MultiListener): - listeners.extend(listener.listeners) - del listener.listeners[:] # type: ignore[attr-defined] - else: - listeners.append(listener) - - self.listeners = listeners - - async def serve( - self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None - ) -> None: - from .. import create_task_group - - async with create_task_group() as tg: - for listener in self.listeners: - tg.start_soon(listener.serve, handler, task_group) - - async def aclose(self) -> None: - for listener in self.listeners: - await listener.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - attributes: dict = {} - for listener in self.listeners: - attributes.update(listener.extra_attributes) - - return attributes diff --git a/venv/lib/python3.12/site-packages/anyio/streams/text.py b/venv/lib/python3.12/site-packages/anyio/streams/text.py deleted file mode 100644 index f1a1127..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/text.py +++ /dev/null @@ -1,147 +0,0 @@ -from __future__ import annotations - -import codecs -from collections.abc import Callable, Mapping -from dataclasses import InitVar, dataclass, field -from typing import Any - -from ..abc import ( - AnyByteReceiveStream, - AnyByteSendStream, - AnyByteStream, - ObjectReceiveStream, - ObjectSendStream, - ObjectStream, -) - - -@dataclass(eq=False) -class TextReceiveStream(ObjectReceiveStream[str]): - """ - Stream wrapper that decodes bytes to strings using the given encoding. - - Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any - completely received unicode characters as soon as they come in. - - :param transport_stream: any bytes-based receive stream - :param encoding: character encoding to use for decoding bytes to strings (defaults - to ``utf-8``) - :param errors: handling scheme for decoding errors (defaults to ``strict``; see the - `codecs module documentation`_ for a comprehensive list of options) - - .. _codecs module documentation: - https://docs.python.org/3/library/codecs.html#codec-objects - """ - - transport_stream: AnyByteReceiveStream - encoding: InitVar[str] = "utf-8" - errors: InitVar[str] = "strict" - _decoder: codecs.IncrementalDecoder = field(init=False) - - def __post_init__(self, encoding: str, errors: str) -> None: - decoder_class = codecs.getincrementaldecoder(encoding) - self._decoder = decoder_class(errors=errors) - - async def receive(self) -> str: - while True: - chunk = await self.transport_stream.receive() - decoded = self._decoder.decode(chunk) - if decoded: - return decoded - - async def aclose(self) -> None: - await self.transport_stream.aclose() - self._decoder.reset() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return self.transport_stream.extra_attributes - - -@dataclass(eq=False) -class TextSendStream(ObjectSendStream[str]): - """ - Sends strings to the wrapped stream as bytes using the given encoding. - - :param AnyByteSendStream transport_stream: any bytes-based send stream - :param str encoding: character encoding to use for encoding strings to bytes - (defaults to ``utf-8``) - :param str errors: handling scheme for encoding errors (defaults to ``strict``; see - the `codecs module documentation`_ for a comprehensive list of options) - - .. _codecs module documentation: - https://docs.python.org/3/library/codecs.html#codec-objects - """ - - transport_stream: AnyByteSendStream - encoding: InitVar[str] = "utf-8" - errors: str = "strict" - _encoder: Callable[..., tuple[bytes, int]] = field(init=False) - - def __post_init__(self, encoding: str) -> None: - self._encoder = codecs.getencoder(encoding) - - async def send(self, item: str) -> None: - encoded = self._encoder(item, self.errors)[0] - await self.transport_stream.send(encoded) - - async def aclose(self) -> None: - await self.transport_stream.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return self.transport_stream.extra_attributes - - -@dataclass(eq=False) -class TextStream(ObjectStream[str]): - """ - A bidirectional stream that decodes bytes to strings on receive and encodes strings - to bytes on send. - - Extra attributes will be provided from both streams, with the receive stream - providing the values in case of a conflict. - - :param AnyByteStream transport_stream: any bytes-based stream - :param str encoding: character encoding to use for encoding/decoding strings to/from - bytes (defaults to ``utf-8``) - :param str errors: handling scheme for encoding errors (defaults to ``strict``; see - the `codecs module documentation`_ for a comprehensive list of options) - - .. _codecs module documentation: - https://docs.python.org/3/library/codecs.html#codec-objects - """ - - transport_stream: AnyByteStream - encoding: InitVar[str] = "utf-8" - errors: InitVar[str] = "strict" - _receive_stream: TextReceiveStream = field(init=False) - _send_stream: TextSendStream = field(init=False) - - def __post_init__(self, encoding: str, errors: str) -> None: - self._receive_stream = TextReceiveStream( - self.transport_stream, encoding=encoding, errors=errors - ) - self._send_stream = TextSendStream( - self.transport_stream, encoding=encoding, errors=errors - ) - - async def receive(self) -> str: - return await self._receive_stream.receive() - - async def send(self, item: str) -> None: - await self._send_stream.send(item) - - async def send_eof(self) -> None: - await self.transport_stream.send_eof() - - async def aclose(self) -> None: - await self._send_stream.aclose() - await self._receive_stream.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return { - **self._send_stream.extra_attributes, - **self._receive_stream.extra_attributes, - } diff --git a/venv/lib/python3.12/site-packages/anyio/streams/tls.py b/venv/lib/python3.12/site-packages/anyio/streams/tls.py deleted file mode 100644 index 70a41cc..0000000 --- a/venv/lib/python3.12/site-packages/anyio/streams/tls.py +++ /dev/null @@ -1,352 +0,0 @@ -from __future__ import annotations - -import logging -import re -import ssl -import sys -from collections.abc import Callable, Mapping -from dataclasses import dataclass -from functools import wraps -from typing import Any, TypeVar - -from .. import ( - BrokenResourceError, - EndOfStream, - aclose_forcefully, - get_cancelled_exc_class, - to_thread, -) -from .._core._typedattr import TypedAttributeSet, typed_attribute -from ..abc import AnyByteStream, ByteStream, Listener, TaskGroup - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") -_PCTRTT = tuple[tuple[str, str], ...] -_PCTRTTT = tuple[_PCTRTT, ...] - - -class TLSAttribute(TypedAttributeSet): - """Contains Transport Layer Security related attributes.""" - - #: the selected ALPN protocol - alpn_protocol: str | None = typed_attribute() - #: the channel binding for type ``tls-unique`` - channel_binding_tls_unique: bytes = typed_attribute() - #: the selected cipher - cipher: tuple[str, str, int] = typed_attribute() - #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert` - # for more information) - peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute() - #: the peer certificate in binary form - peer_certificate_binary: bytes | None = typed_attribute() - #: ``True`` if this is the server side of the connection - server_side: bool = typed_attribute() - #: ciphers shared by the client during the TLS handshake (``None`` if this is the - #: client side) - shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute() - #: the :class:`~ssl.SSLObject` used for encryption - ssl_object: ssl.SSLObject = typed_attribute() - #: ``True`` if this stream does (and expects) a closing TLS handshake when the - #: stream is being closed - standard_compatible: bool = typed_attribute() - #: the TLS protocol version (e.g. ``TLSv1.2``) - tls_version: str = typed_attribute() - - -@dataclass(eq=False) -class TLSStream(ByteStream): - """ - A stream wrapper that encrypts all sent data and decrypts received data. - - This class has no public initializer; use :meth:`wrap` instead. - All extra attributes from :class:`~TLSAttribute` are supported. - - :var AnyByteStream transport_stream: the wrapped stream - - """ - - transport_stream: AnyByteStream - standard_compatible: bool - _ssl_object: ssl.SSLObject - _read_bio: ssl.MemoryBIO - _write_bio: ssl.MemoryBIO - - @classmethod - async def wrap( - cls, - transport_stream: AnyByteStream, - *, - server_side: bool | None = None, - hostname: str | None = None, - ssl_context: ssl.SSLContext | None = None, - standard_compatible: bool = True, - ) -> TLSStream: - """ - Wrap an existing stream with Transport Layer Security. - - This performs a TLS handshake with the peer. - - :param transport_stream: a bytes-transporting stream to wrap - :param server_side: ``True`` if this is the server side of the connection, - ``False`` if this is the client side (if omitted, will be set to ``False`` - if ``hostname`` has been provided, ``False`` otherwise). Used only to create - a default context when an explicit context has not been provided. - :param hostname: host name of the peer (if host name checking is desired) - :param ssl_context: the SSLContext object to use (if not provided, a secure - default will be created) - :param standard_compatible: if ``False``, skip the closing handshake when - closing the connection, and don't raise an exception if the peer does the - same - :raises ~ssl.SSLError: if the TLS handshake fails - - """ - if server_side is None: - server_side = not hostname - - if not ssl_context: - purpose = ( - ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH - ) - ssl_context = ssl.create_default_context(purpose) - - # Re-enable detection of unexpected EOFs if it was disabled by Python - if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): - ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF - - bio_in = ssl.MemoryBIO() - bio_out = ssl.MemoryBIO() - - # External SSLContext implementations may do blocking I/O in wrap_bio(), - # but the standard library implementation won't - if type(ssl_context) is ssl.SSLContext: - ssl_object = ssl_context.wrap_bio( - bio_in, bio_out, server_side=server_side, server_hostname=hostname - ) - else: - ssl_object = await to_thread.run_sync( - ssl_context.wrap_bio, - bio_in, - bio_out, - server_side, - hostname, - None, - ) - - wrapper = cls( - transport_stream=transport_stream, - standard_compatible=standard_compatible, - _ssl_object=ssl_object, - _read_bio=bio_in, - _write_bio=bio_out, - ) - await wrapper._call_sslobject_method(ssl_object.do_handshake) - return wrapper - - async def _call_sslobject_method( - self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] - ) -> T_Retval: - while True: - try: - result = func(*args) - except ssl.SSLWantReadError: - try: - # Flush any pending writes first - if self._write_bio.pending: - await self.transport_stream.send(self._write_bio.read()) - - data = await self.transport_stream.receive() - except EndOfStream: - self._read_bio.write_eof() - except OSError as exc: - self._read_bio.write_eof() - self._write_bio.write_eof() - raise BrokenResourceError from exc - else: - self._read_bio.write(data) - except ssl.SSLWantWriteError: - await self.transport_stream.send(self._write_bio.read()) - except ssl.SSLSyscallError as exc: - self._read_bio.write_eof() - self._write_bio.write_eof() - raise BrokenResourceError from exc - except ssl.SSLError as exc: - self._read_bio.write_eof() - self._write_bio.write_eof() - if isinstance(exc, ssl.SSLEOFError) or ( - exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror - ): - if self.standard_compatible: - raise BrokenResourceError from exc - else: - raise EndOfStream from None - - raise - else: - # Flush any pending writes first - if self._write_bio.pending: - await self.transport_stream.send(self._write_bio.read()) - - return result - - async def unwrap(self) -> tuple[AnyByteStream, bytes]: - """ - Does the TLS closing handshake. - - :return: a tuple of (wrapped byte stream, bytes left in the read buffer) - - """ - await self._call_sslobject_method(self._ssl_object.unwrap) - self._read_bio.write_eof() - self._write_bio.write_eof() - return self.transport_stream, self._read_bio.read() - - async def aclose(self) -> None: - if self.standard_compatible: - try: - await self.unwrap() - except BaseException: - await aclose_forcefully(self.transport_stream) - raise - - await self.transport_stream.aclose() - - async def receive(self, max_bytes: int = 65536) -> bytes: - data = await self._call_sslobject_method(self._ssl_object.read, max_bytes) - if not data: - raise EndOfStream - - return data - - async def send(self, item: bytes) -> None: - await self._call_sslobject_method(self._ssl_object.write, item) - - async def send_eof(self) -> None: - tls_version = self.extra(TLSAttribute.tls_version) - match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version) - if match: - major, minor = int(match.group(1)), int(match.group(2) or 0) - if (major, minor) < (1, 3): - raise NotImplementedError( - f"send_eof() requires at least TLSv1.3; current " - f"session uses {tls_version}" - ) - - raise NotImplementedError( - "send_eof() has not yet been implemented for TLS streams" - ) - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return { - **self.transport_stream.extra_attributes, - TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol, - TLSAttribute.channel_binding_tls_unique: ( - self._ssl_object.get_channel_binding - ), - TLSAttribute.cipher: self._ssl_object.cipher, - TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False), - TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert( - True - ), - TLSAttribute.server_side: lambda: self._ssl_object.server_side, - TLSAttribute.shared_ciphers: lambda: self._ssl_object.shared_ciphers() - if self._ssl_object.server_side - else None, - TLSAttribute.standard_compatible: lambda: self.standard_compatible, - TLSAttribute.ssl_object: lambda: self._ssl_object, - TLSAttribute.tls_version: self._ssl_object.version, - } - - -@dataclass(eq=False) -class TLSListener(Listener[TLSStream]): - """ - A convenience listener that wraps another listener and auto-negotiates a TLS session - on every accepted connection. - - If the TLS handshake times out or raises an exception, - :meth:`handle_handshake_error` is called to do whatever post-mortem processing is - deemed necessary. - - Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute. - - :param Listener listener: the listener to wrap - :param ssl_context: the SSL context object - :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap` - :param handshake_timeout: time limit for the TLS handshake - (passed to :func:`~anyio.fail_after`) - """ - - listener: Listener[Any] - ssl_context: ssl.SSLContext - standard_compatible: bool = True - handshake_timeout: float = 30 - - @staticmethod - async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None: - """ - Handle an exception raised during the TLS handshake. - - This method does 3 things: - - #. Forcefully closes the original stream - #. Logs the exception (unless it was a cancellation exception) using the - ``anyio.streams.tls`` logger - #. Reraises the exception if it was a base exception or a cancellation exception - - :param exc: the exception - :param stream: the original stream - - """ - await aclose_forcefully(stream) - - # Log all except cancellation exceptions - if not isinstance(exc, get_cancelled_exc_class()): - # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using - # any asyncio implementation, so we explicitly pass the exception to log - # (https://github.com/python/cpython/issues/108668). Trio does not have this - # issue because it works around the CPython bug. - logging.getLogger(__name__).exception( - "Error during TLS handshake", exc_info=exc - ) - - # Only reraise base exceptions and cancellation exceptions - if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()): - raise - - async def serve( - self, - handler: Callable[[TLSStream], Any], - task_group: TaskGroup | None = None, - ) -> None: - @wraps(handler) - async def handler_wrapper(stream: AnyByteStream) -> None: - from .. import fail_after - - try: - with fail_after(self.handshake_timeout): - wrapped_stream = await TLSStream.wrap( - stream, - ssl_context=self.ssl_context, - standard_compatible=self.standard_compatible, - ) - except BaseException as exc: - await self.handle_handshake_error(exc, stream) - else: - await handler(wrapped_stream) - - await self.listener.serve(handler_wrapper, task_group) - - async def aclose(self) -> None: - await self.listener.aclose() - - @property - def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: - return { - TLSAttribute.standard_compatible: lambda: self.standard_compatible, - } diff --git a/venv/lib/python3.12/site-packages/anyio/to_interpreter.py b/venv/lib/python3.12/site-packages/anyio/to_interpreter.py deleted file mode 100644 index 8a2e993..0000000 --- a/venv/lib/python3.12/site-packages/anyio/to_interpreter.py +++ /dev/null @@ -1,218 +0,0 @@ -from __future__ import annotations - -import atexit -import os -import pickle -import sys -from collections import deque -from collections.abc import Callable -from textwrap import dedent -from typing import Any, Final, TypeVar - -from . import current_time, to_thread -from ._core._exceptions import BrokenWorkerIntepreter -from ._core._synchronization import CapacityLimiter -from .lowlevel import RunVar - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib -FMT_UNPICKLED: Final = 0 -FMT_PICKLED: Final = 1 -DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value -MAX_WORKER_IDLE_TIME = ( - 30 # seconds a subinterpreter can be idle before becoming eligible for pruning -) - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") - -_idle_workers = RunVar[deque["Worker"]]("_available_workers") -_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter") - - -class Worker: - _run_func = compile( - dedent(""" - import _interpqueues as queues - import _interpreters as interpreters - from pickle import loads, dumps, HIGHEST_PROTOCOL - - item = queues.get(queue_id)[0] - try: - func, args = loads(item) - retval = func(*args) - except BaseException as exc: - is_exception = True - retval = exc - else: - is_exception = False - - try: - queues.put(queue_id, (retval, is_exception), FMT_UNPICKLED, UNBOUND) - except interpreters.NotShareableError: - retval = dumps(retval, HIGHEST_PROTOCOL) - queues.put(queue_id, (retval, is_exception), FMT_PICKLED, UNBOUND) - """), - "", - "exec", - ) - - last_used: float = 0 - - _initialized: bool = False - _interpreter_id: int - _queue_id: int - - def initialize(self) -> None: - import _interpqueues as queues - import _interpreters as interpreters - - self._interpreter_id = interpreters.create() - self._queue_id = queues.create(2, FMT_UNPICKLED, UNBOUND) - self._initialized = True - interpreters.set___main___attrs( - self._interpreter_id, - { - "queue_id": self._queue_id, - "FMT_PICKLED": FMT_PICKLED, - "FMT_UNPICKLED": FMT_UNPICKLED, - "UNBOUND": UNBOUND, - }, - ) - - def destroy(self) -> None: - import _interpqueues as queues - import _interpreters as interpreters - - if self._initialized: - interpreters.destroy(self._interpreter_id) - queues.destroy(self._queue_id) - - def _call( - self, - func: Callable[..., T_Retval], - args: tuple[Any], - ) -> tuple[Any, bool]: - import _interpqueues as queues - import _interpreters as interpreters - - if not self._initialized: - self.initialize() - - payload = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL) - queues.put(self._queue_id, payload, FMT_PICKLED, UNBOUND) - - res: Any - is_exception: bool - if exc_info := interpreters.exec(self._interpreter_id, self._run_func): - raise BrokenWorkerIntepreter(exc_info) - - (res, is_exception), fmt = queues.get(self._queue_id)[:2] - if fmt == FMT_PICKLED: - res = pickle.loads(res) - - return res, is_exception - - async def call( - self, - func: Callable[..., T_Retval], - args: tuple[Any], - limiter: CapacityLimiter, - ) -> T_Retval: - result, is_exception = await to_thread.run_sync( - self._call, - func, - args, - limiter=limiter, - ) - if is_exception: - raise result - - return result - - -def _stop_workers(workers: deque[Worker]) -> None: - for worker in workers: - worker.destroy() - - workers.clear() - - -async def run_sync( - func: Callable[[Unpack[PosArgsT]], T_Retval], - *args: Unpack[PosArgsT], - limiter: CapacityLimiter | None = None, -) -> T_Retval: - """ - Call the given function with the given arguments in a subinterpreter. - - If the ``cancellable`` option is enabled and the task waiting for its completion is - cancelled, the call will still run its course but its return value (or any raised - exception) will be ignored. - - .. warning:: This feature is **experimental**. The upstream interpreter API has not - yet been finalized or thoroughly tested, so don't rely on this for anything - mission critical. - - :param func: a callable - :param args: positional arguments for the callable - :param limiter: capacity limiter to use to limit the total amount of subinterpreters - running (if omitted, the default limiter is used) - :return: the result of the call - :raises BrokenWorkerIntepreter: if there's an internal error in a subinterpreter - - """ - if sys.version_info <= (3, 13): - raise RuntimeError("subinterpreters require at least Python 3.13") - - if limiter is None: - limiter = current_default_interpreter_limiter() - - try: - idle_workers = _idle_workers.get() - except LookupError: - idle_workers = deque() - _idle_workers.set(idle_workers) - atexit.register(_stop_workers, idle_workers) - - async with limiter: - try: - worker = idle_workers.pop() - except IndexError: - worker = Worker() - - try: - return await worker.call(func, args, limiter) - finally: - # Prune workers that have been idle for too long - now = current_time() - while idle_workers: - if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME: - break - - await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter) - - worker.last_used = current_time() - idle_workers.append(worker) - - -def current_default_interpreter_limiter() -> CapacityLimiter: - """ - Return the capacity limiter that is used by default to limit the number of - concurrently running subinterpreters. - - Defaults to the number of CPU cores. - - :return: a capacity limiter object - - """ - try: - return _default_interpreter_limiter.get() - except LookupError: - limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT) - _default_interpreter_limiter.set(limiter) - return limiter diff --git a/venv/lib/python3.12/site-packages/anyio/to_process.py b/venv/lib/python3.12/site-packages/anyio/to_process.py deleted file mode 100644 index 495de2a..0000000 --- a/venv/lib/python3.12/site-packages/anyio/to_process.py +++ /dev/null @@ -1,258 +0,0 @@ -from __future__ import annotations - -import os -import pickle -import subprocess -import sys -from collections import deque -from collections.abc import Callable -from importlib.util import module_from_spec, spec_from_file_location -from typing import TypeVar, cast - -from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class -from ._core._exceptions import BrokenWorkerProcess -from ._core._subprocesses import open_process -from ._core._synchronization import CapacityLimiter -from ._core._tasks import CancelScope, fail_after -from .abc import ByteReceiveStream, ByteSendStream, Process -from .lowlevel import RunVar, checkpoint_if_cancelled -from .streams.buffered import BufferedByteReceiveStream - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -WORKER_MAX_IDLE_TIME = 300 # 5 minutes - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") - -_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers") -_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar( - "_process_pool_idle_workers" -) -_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") - - -async def run_sync( # type: ignore[return] - func: Callable[[Unpack[PosArgsT]], T_Retval], - *args: Unpack[PosArgsT], - cancellable: bool = False, - limiter: CapacityLimiter | None = None, -) -> T_Retval: - """ - Call the given function with the given arguments in a worker process. - - If the ``cancellable`` option is enabled and the task waiting for its completion is - cancelled, the worker process running it will be abruptly terminated using SIGKILL - (or ``terminateProcess()`` on Windows). - - :param func: a callable - :param args: positional arguments for the callable - :param cancellable: ``True`` to allow cancellation of the operation while it's - running - :param limiter: capacity limiter to use to limit the total amount of processes - running (if omitted, the default limiter is used) - :return: an awaitable that yields the return value of the function. - - """ - - async def send_raw_command(pickled_cmd: bytes) -> object: - try: - await stdin.send(pickled_cmd) - response = await buffered.receive_until(b"\n", 50) - status, length = response.split(b" ") - if status not in (b"RETURN", b"EXCEPTION"): - raise RuntimeError( - f"Worker process returned unexpected response: {response!r}" - ) - - pickled_response = await buffered.receive_exactly(int(length)) - except BaseException as exc: - workers.discard(process) - try: - process.kill() - with CancelScope(shield=True): - await process.aclose() - except ProcessLookupError: - pass - - if isinstance(exc, get_cancelled_exc_class()): - raise - else: - raise BrokenWorkerProcess from exc - - retval = pickle.loads(pickled_response) - if status == b"EXCEPTION": - assert isinstance(retval, BaseException) - raise retval - else: - return retval - - # First pickle the request before trying to reserve a worker process - await checkpoint_if_cancelled() - request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL) - - # If this is the first run in this event loop thread, set up the necessary variables - try: - workers = _process_pool_workers.get() - idle_workers = _process_pool_idle_workers.get() - except LookupError: - workers = set() - idle_workers = deque() - _process_pool_workers.set(workers) - _process_pool_idle_workers.set(idle_workers) - get_async_backend().setup_process_pool_exit_at_shutdown(workers) - - async with limiter or current_default_process_limiter(): - # Pop processes from the pool (starting from the most recently used) until we - # find one that hasn't exited yet - process: Process - while idle_workers: - process, idle_since = idle_workers.pop() - if process.returncode is None: - stdin = cast(ByteSendStream, process.stdin) - buffered = BufferedByteReceiveStream( - cast(ByteReceiveStream, process.stdout) - ) - - # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME - # seconds or longer - now = current_time() - killed_processes: list[Process] = [] - while idle_workers: - if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME: - break - - process_to_kill, idle_since = idle_workers.popleft() - process_to_kill.kill() - workers.remove(process_to_kill) - killed_processes.append(process_to_kill) - - with CancelScope(shield=True): - for killed_process in killed_processes: - await killed_process.aclose() - - break - - workers.remove(process) - else: - command = [sys.executable, "-u", "-m", __name__] - process = await open_process( - command, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - try: - stdin = cast(ByteSendStream, process.stdin) - buffered = BufferedByteReceiveStream( - cast(ByteReceiveStream, process.stdout) - ) - with fail_after(20): - message = await buffered.receive(6) - - if message != b"READY\n": - raise BrokenWorkerProcess( - f"Worker process returned unexpected response: {message!r}" - ) - - main_module_path = getattr(sys.modules["__main__"], "__file__", None) - pickled = pickle.dumps( - ("init", sys.path, main_module_path), - protocol=pickle.HIGHEST_PROTOCOL, - ) - await send_raw_command(pickled) - except (BrokenWorkerProcess, get_cancelled_exc_class()): - raise - except BaseException as exc: - process.kill() - raise BrokenWorkerProcess( - "Error during worker process initialization" - ) from exc - - workers.add(process) - - with CancelScope(shield=not cancellable): - try: - return cast(T_Retval, await send_raw_command(request)) - finally: - if process in workers: - idle_workers.append((process, current_time())) - - -def current_default_process_limiter() -> CapacityLimiter: - """ - Return the capacity limiter that is used by default to limit the number of worker - processes. - - :return: a capacity limiter object - - """ - try: - return _default_process_limiter.get() - except LookupError: - limiter = CapacityLimiter(os.cpu_count() or 2) - _default_process_limiter.set(limiter) - return limiter - - -def process_worker() -> None: - # Redirect standard streams to os.devnull so that user code won't interfere with the - # parent-worker communication - stdin = sys.stdin - stdout = sys.stdout - sys.stdin = open(os.devnull) - sys.stdout = open(os.devnull, "w") - - stdout.buffer.write(b"READY\n") - while True: - retval = exception = None - try: - command, *args = pickle.load(stdin.buffer) - except EOFError: - return - except BaseException as exc: - exception = exc - else: - if command == "run": - func, args = args - try: - retval = func(*args) - except BaseException as exc: - exception = exc - elif command == "init": - main_module_path: str | None - sys.path, main_module_path = args - del sys.modules["__main__"] - if main_module_path and os.path.isfile(main_module_path): - # Load the parent's main module but as __mp_main__ instead of - # __main__ (like multiprocessing does) to avoid infinite recursion - try: - spec = spec_from_file_location("__mp_main__", main_module_path) - if spec and spec.loader: - main = module_from_spec(spec) - spec.loader.exec_module(main) - sys.modules["__main__"] = main - except BaseException as exc: - exception = exc - try: - if exception is not None: - status = b"EXCEPTION" - pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL) - else: - status = b"RETURN" - pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL) - except BaseException as exc: - exception = exc - status = b"EXCEPTION" - pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL) - - stdout.buffer.write(b"%s %d\n" % (status, len(pickled))) - stdout.buffer.write(pickled) - - # Respect SIGTERM - if isinstance(exception, SystemExit): - raise exception - - -if __name__ == "__main__": - process_worker() diff --git a/venv/lib/python3.12/site-packages/anyio/to_thread.py b/venv/lib/python3.12/site-packages/anyio/to_thread.py deleted file mode 100644 index 5070516..0000000 --- a/venv/lib/python3.12/site-packages/anyio/to_thread.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -import sys -from collections.abc import Callable -from typing import TypeVar -from warnings import warn - -from ._core._eventloop import get_async_backend -from .abc import CapacityLimiter - -if sys.version_info >= (3, 11): - from typing import TypeVarTuple, Unpack -else: - from typing_extensions import TypeVarTuple, Unpack - -T_Retval = TypeVar("T_Retval") -PosArgsT = TypeVarTuple("PosArgsT") - - -async def run_sync( - func: Callable[[Unpack[PosArgsT]], T_Retval], - *args: Unpack[PosArgsT], - abandon_on_cancel: bool = False, - cancellable: bool | None = None, - limiter: CapacityLimiter | None = None, -) -> T_Retval: - """ - Call the given function with the given arguments in a worker thread. - - If the ``cancellable`` option is enabled and the task waiting for its completion is - cancelled, the thread will still run its course but its return value (or any raised - exception) will be ignored. - - :param func: a callable - :param args: positional arguments for the callable - :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run - unchecked on own) if the host task is cancelled, ``False`` to ignore - cancellations in the host task until the operation has completed in the worker - thread - :param cancellable: deprecated alias of ``abandon_on_cancel``; will override - ``abandon_on_cancel`` if both parameters are passed - :param limiter: capacity limiter to use to limit the total amount of threads running - (if omitted, the default limiter is used) - :return: an awaitable that yields the return value of the function. - - """ - if cancellable is not None: - abandon_on_cancel = cancellable - warn( - "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " - "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", - DeprecationWarning, - stacklevel=2, - ) - - return await get_async_backend().run_sync_in_worker_thread( - func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter - ) - - -def current_default_thread_limiter() -> CapacityLimiter: - """ - Return the capacity limiter that is used by default to limit the number of - concurrent threads. - - :return: a capacity limiter object - - """ - return get_async_backend().current_default_thread_limiter() diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/METADATA b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/METADATA deleted file mode 100644 index 69be300..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/METADATA +++ /dev/null @@ -1,123 +0,0 @@ -Metadata-Version: 2.4 -Name: beautifulsoup4 -Version: 4.13.3 -Summary: Screen-scraping library -Project-URL: Download, https://www.crummy.com/software/BeautifulSoup/bs4/download/ -Project-URL: Homepage, https://www.crummy.com/software/BeautifulSoup/bs4/ -Author-email: Leonard Richardson -License: MIT License -License-File: AUTHORS -License-File: LICENSE -Keywords: HTML,XML,parse,soup -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: Text Processing :: Markup :: HTML -Classifier: Topic :: Text Processing :: Markup :: SGML -Classifier: Topic :: Text Processing :: Markup :: XML -Requires-Python: >=3.7.0 -Requires-Dist: soupsieve>1.2 -Requires-Dist: typing-extensions>=4.0.0 -Provides-Extra: cchardet -Requires-Dist: cchardet; extra == 'cchardet' -Provides-Extra: chardet -Requires-Dist: chardet; extra == 'chardet' -Provides-Extra: charset-normalizer -Requires-Dist: charset-normalizer; extra == 'charset-normalizer' -Provides-Extra: html5lib -Requires-Dist: html5lib; extra == 'html5lib' -Provides-Extra: lxml -Requires-Dist: lxml; extra == 'lxml' -Description-Content-Type: text/markdown - -Beautiful Soup is a library that makes it easy to scrape information -from web pages. It sits atop an HTML or XML parser, providing Pythonic -idioms for iterating, searching, and modifying the parse tree. - -# Quick start - -``` ->>> from bs4 import BeautifulSoup ->>> soup = BeautifulSoup("

SomebadHTML") ->>> print(soup.prettify()) - - -

- Some - - bad - - HTML - - -

- - ->>> soup.find(string="bad") -'bad' ->>> soup.i -HTML -# ->>> soup = BeautifulSoup("SomebadXML", "xml") -# ->>> print(soup.prettify()) - - - Some - - bad - - XML - - -``` - -To go beyond the basics, [comprehensive documentation is available](https://www.crummy.com/software/BeautifulSoup/bs4/doc/). - -# Links - -* [Homepage](https://www.crummy.com/software/BeautifulSoup/bs4/) -* [Documentation](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) -* [Discussion group](https://groups.google.com/group/beautifulsoup/) -* [Development](https://code.launchpad.net/beautifulsoup/) -* [Bug tracker](https://bugs.launchpad.net/beautifulsoup/) -* [Complete changelog](https://git.launchpad.net/beautifulsoup/tree/CHANGELOG) - -# Note on Python 2 sunsetting - -Beautiful Soup's support for Python 2 was discontinued on December 31, -2020: one year after the sunset date for Python 2 itself. From this -point onward, new Beautiful Soup development will exclusively target -Python 3. The final release of Beautiful Soup 4 to support Python 2 -was 4.9.3. - -# Supporting the project - -If you use Beautiful Soup as part of your professional work, please consider a -[Tidelift subscription](https://tidelift.com/subscription/pkg/pypi-beautifulsoup4?utm_source=pypi-beautifulsoup4&utm_medium=referral&utm_campaign=readme). -This will support many of the free software projects your organization -depends on, not just Beautiful Soup. - -If you use Beautiful Soup for personal projects, the best way to say -thank you is to read -[Tool Safety](https://www.crummy.com/software/BeautifulSoup/zine/), a zine I -wrote about what Beautiful Soup has taught me about software -development. - -# Building the documentation - -The bs4/doc/ directory contains full documentation in Sphinx -format. Run `make html` in that directory to create HTML -documentation. - -# Running the unit tests - -Beautiful Soup supports unit test discovery using Pytest: - -``` -$ pytest -``` - diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/RECORD b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/RECORD deleted file mode 100644 index a3efc0d..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/RECORD +++ /dev/null @@ -1,89 +0,0 @@ -beautifulsoup4-4.13.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -beautifulsoup4-4.13.3.dist-info/METADATA,sha256=o692i819qmuScSS6UxoBFAi2xPSl8bk2V6TuQ3zBofs,3809 -beautifulsoup4-4.13.3.dist-info/RECORD,, -beautifulsoup4-4.13.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 -beautifulsoup4-4.13.3.dist-info/licenses/AUTHORS,sha256=6-a5uw17L-xMAg7-R3iVPGKH_OwwacpjRkuOVPjAeyw,2198 -beautifulsoup4-4.13.3.dist-info/licenses/LICENSE,sha256=VbTY1LHlvIbRDvrJG3TIe8t3UmsPW57a-LnNKtxzl7I,1441 -bs4/__init__.py,sha256=-jvrE9GBtzsOF3wIrIOALQTqu99mf9_gEhNFJMCQLeg,44212 -bs4/__pycache__/__init__.cpython-312.pyc,, -bs4/__pycache__/_deprecation.cpython-312.pyc,, -bs4/__pycache__/_typing.cpython-312.pyc,, -bs4/__pycache__/_warnings.cpython-312.pyc,, -bs4/__pycache__/css.cpython-312.pyc,, -bs4/__pycache__/dammit.cpython-312.pyc,, -bs4/__pycache__/diagnose.cpython-312.pyc,, -bs4/__pycache__/element.cpython-312.pyc,, -bs4/__pycache__/exceptions.cpython-312.pyc,, -bs4/__pycache__/filter.cpython-312.pyc,, -bs4/__pycache__/formatter.cpython-312.pyc,, -bs4/_deprecation.py,sha256=ucZjfBAUF1B0f5ldNIIhlkHsYjHtvwELWlE3_pAR6Vs,2394 -bs4/_typing.py,sha256=3FgPPPrdsTa-kvn1R36o1k_2SfilcUWm4M9i7G4qFl8,7118 -bs4/_warnings.py,sha256=ZuOETgcnEbZgw2N0nnNXn6wvtrn2ut7AF0d98bvkMFc,4711 -bs4/builder/__init__.py,sha256=TYAKmGFuVfTsI53reHijcZKETnPuvse57KZ6LsZsJRo,31130 -bs4/builder/__pycache__/__init__.cpython-312.pyc,, -bs4/builder/__pycache__/_html5lib.cpython-312.pyc,, -bs4/builder/__pycache__/_htmlparser.cpython-312.pyc,, -bs4/builder/__pycache__/_lxml.cpython-312.pyc,, -bs4/builder/_html5lib.py,sha256=3MXq29SYg9XoS9gu2hgTDU02IQkv8kIBx3rW1QWY3fg,22846 -bs4/builder/_htmlparser.py,sha256=cu9PFkxkqVIIe9nU3fVy-JJhINEhY8cGbsuCwZCnQCA,17872 -bs4/builder/_lxml.py,sha256=XRzCA4WzvIUjJk9_U4kWzMBvGokr_UaIvoGUmtLtTYI,18538 -bs4/css.py,sha256=XGQq7HQUDyYEbDorFMGIGek7QGPiFuZYnvNEQ59GyxM,12685 -bs4/dammit.py,sha256=oHd1elJ44kMobBGSQRuG7Wln6M-BLz1unOuUscaL9h0,51472 -bs4/diagnose.py,sha256=zy7_GPQHsTtNf8s10WWIRcC5xH5_8LKs295Aa7iFUyI,7832 -bs4/element.py,sha256=8CXiRqz2DZJyga2igCVGaXdP7urNEDvDnsRid3SNNw4,109331 -bs4/exceptions.py,sha256=Q9FOadNe8QRvzDMaKSXe2Wtl8JK_oAZW7mbFZBVP_GE,951 -bs4/filter.py,sha256=2_ydSe978oLVmVyNLBi09Cc1VJEXYVjuO6K4ALq6XFk,28819 -bs4/formatter.py,sha256=5O4gBxTTi5TLU6TdqsgYI9Io0Gc_6-oCAWpfHI3Thn0,10464 -bs4/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -bs4/tests/__init__.py,sha256=Heh-lB8w8mzpaWcgs7MRwkBnDcf1YxAvqvePmsej1Pc,52268 -bs4/tests/__pycache__/__init__.cpython-312.pyc,, -bs4/tests/__pycache__/test_builder.cpython-312.pyc,, -bs4/tests/__pycache__/test_builder_registry.cpython-312.pyc,, -bs4/tests/__pycache__/test_css.cpython-312.pyc,, -bs4/tests/__pycache__/test_dammit.cpython-312.pyc,, -bs4/tests/__pycache__/test_element.cpython-312.pyc,, -bs4/tests/__pycache__/test_filter.cpython-312.pyc,, -bs4/tests/__pycache__/test_formatter.cpython-312.pyc,, -bs4/tests/__pycache__/test_fuzz.cpython-312.pyc,, -bs4/tests/__pycache__/test_html5lib.cpython-312.pyc,, -bs4/tests/__pycache__/test_htmlparser.cpython-312.pyc,, -bs4/tests/__pycache__/test_lxml.cpython-312.pyc,, -bs4/tests/__pycache__/test_navigablestring.cpython-312.pyc,, -bs4/tests/__pycache__/test_pageelement.cpython-312.pyc,, -bs4/tests/__pycache__/test_soup.cpython-312.pyc,, -bs4/tests/__pycache__/test_tag.cpython-312.pyc,, -bs4/tests/__pycache__/test_tree.cpython-312.pyc,, -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase,sha256=yUdXkbpNK7LVOQ0LBHMoqZ1rWaBfSXWytoO_xdSm7Ho,15 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4818336571064320.testcase,sha256=Uv_dx4a43TSfoNkjU-jHW2nSXkqHFg4XdAw7SWVObUk,23 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4999465949331456.testcase,sha256=OEyVA0Ej4FxswOElrUNt0In4s4YhrmtaxE_NHGZvGtg,30 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5000587759190016.testcase,sha256=G4vpNBOz-RwMpi6ewEgNEa13zX0sXhmL7VHOyIcdKVQ,15347 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5167584867909632.testcase,sha256=3d8z65o4p7Rur-RmCHoOjzqaYQ8EAtjmiBYTHNyAdl4,19469 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5270998950477824.testcase,sha256=NfGIlit1k40Ip3mlnBkYOkIDJX6gHtjlErwl7gsBjAQ,12 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5375146639360000.testcase,sha256=xy4i1U0nhFHcnyc5pRKS6JRMvuoCNUur-Scor6UxIGw,4317 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5492400320282624.testcase,sha256=Q-UTYpQBUsWoMgIUspUlzveSI-41s4ABC3jajRb-K0o,11502 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5703933063462912.testcase,sha256=2bq3S8KxZgk8EajLReHD8m4_0Lj_nrkyJAxB_z_U0D0,5 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5843991618256896.testcase,sha256=MZDu31LPLfgu6jP9IZkrlwNes3f_sL8WFP5BChkUKdY,35 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5984173902397440.testcase,sha256=w58r-s6besG5JwPXpnz37W2YTj9-_qxFbk6hiEnKeIQ,51495 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6124268085182464.testcase,sha256=q8rkdMECEXKcqVhOf5zWHkSBTQeOPt0JiLg2TZiPCuk,10380 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6241471367348224.testcase,sha256=QfzoOxKwNuqG-4xIrea6MOQLXhfAAOQJ0r9u-J6kSNs,19 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6306874195312640.testcase,sha256=MJ2pHFuuCQUiQz1Kor2sof7LWeRERQ6QK43YNqQHg9o,47 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6450958476902400.testcase,sha256=EItOpSdeD4ewK-qgJ9vtxennwn_huguzXgctrUT7fqE,3546 -bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6600557255327744.testcase,sha256=a2aJTG4FceGSJXsjtxoS8S4jk_8rZsS3aznLkeO2_dY,124 -bs4/tests/fuzz/crash-0d306a50c8ed8bcd0785b67000fcd5dea1d33f08.testcase,sha256=jRFRtCKlP3-3EDLc_iVRTcE6JNymv0rYcVM6qRaPrxI,2607 -bs4/tests/fuzz/crash-ffbdfa8a2b26f13537b68d3794b0478a4090ee4a.testcase,sha256=7NsdCiXWAhNkmoW1pvF7rbZExyLAQIWtDtSHXIsH6YU,103 -bs4/tests/test_builder.py,sha256=BBMBirb4mb-fVdJj4dxQCxrdcjQeulKSKBFrPFVpVOk,1095 -bs4/tests/test_builder_registry.py,sha256=tpJ5Niva_cF49SdzIb1gMo0W4Tiodr8BYSOE3O6P_g8,5064 -bs4/tests/test_css.py,sha256=T_HCMzpe6hTr8d2YFXm0DScr8gT8d6h0MYlhZfo6A4U,18625 -bs4/tests/test_dammit.py,sha256=TQCVe6kKVYSuYjwTtIvIaOYYmWYPMnR_3PK45kimLg4,17840 -bs4/tests/test_element.py,sha256=u7FbTtKE6pYJetD1PgS3fCU1-QQXfB7GaLwfI3s4ROY,4373 -bs4/tests/test_filter.py,sha256=Sie2l-vepWTAqlXJJpG0Qp4HD8HHSi2TC1XymCxws70,27032 -bs4/tests/test_formatter.py,sha256=a6TaeNOVeg_ZYseiP7atmFyYJkQJqlk-jlVxMlyJC2o,6943 -bs4/tests/test_fuzz.py,sha256=zyaoWgCt8hnRkXecBYM9x91fI_Ao9eQUcsBi76ooJ08,7123 -bs4/tests/test_html5lib.py,sha256=ljMOAds__k9zhfT4jVnxxhZkLEggaT7wqDexzDNwus4,9206 -bs4/tests/test_htmlparser.py,sha256=iDHEI69GcisNP48BeHdLAWlqPGhrBwxftnUM8_3nsR4,6662 -bs4/tests/test_lxml.py,sha256=4fZIsNVbm2zdRQFNNwD-lqwf_QtUtiU4QbtLXISQZBw,7453 -bs4/tests/test_navigablestring.py,sha256=ntfnbp8-sRAOoCCVbm4cCXatS7kmCOaIRFDj-v5-l0s,5096 -bs4/tests/test_pageelement.py,sha256=lAw-sVP3zJX0VdHXXN1Ia3tci5dgK10Gac5o9G46IIk,16195 -bs4/tests/test_soup.py,sha256=I-mhNheo2-PTvfJToDI43EO4RmGlpKJsYOS19YoQ7-8,22669 -bs4/tests/test_tag.py,sha256=ue32hxQs_a1cMuzyu7MNjK42t0IOGMA6POPLIArMOts,9690 -bs4/tests/test_tree.py,sha256=vgUa6x8AJFEvHQ7RQu0973wrsLCRdRpdtq4oZAa_ANA,54839 diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/WHEEL b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/WHEEL deleted file mode 100644 index 12228d4..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.27.0 -Root-Is-Purelib: true -Tag: py3-none-any diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/AUTHORS b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/AUTHORS deleted file mode 100644 index 587a979..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/AUTHORS +++ /dev/null @@ -1,49 +0,0 @@ -Behold, mortal, the origins of Beautiful Soup... -================================================ - -Leonard Richardson is the primary maintainer. - -Aaron DeVore, Isaac Muse and Chris Papademetrious have made -significant contributions to the code base. - -Mark Pilgrim provided the encoding detection code that forms the base -of UnicodeDammit. - -Thomas Kluyver and Ezio Melotti finished the work of getting Beautiful -Soup 4 working under Python 3. - -Simon Willison wrote soupselect, which was used to make Beautiful Soup -support CSS selectors. Isaac Muse wrote SoupSieve, which made it -possible to _remove_ the CSS selector code from Beautiful Soup. - -Sam Ruby helped with a lot of edge cases. - -Jonathan Ellis was awarded the prestigious Beau Potage D'Or for his -work in solving the nestable tags conundrum. - -An incomplete list of people have contributed patches to Beautiful -Soup: - - Istvan Albert, Andrew Lin, Anthony Baxter, Oliver Beattie, Andrew -Boyko, Tony Chang, Francisco Canas, "Delong", Zephyr Fang, Fuzzy, -Roman Gaufman, Yoni Gilad, Richie Hindle, Toshihiro Kamiya, Peteris -Krumins, Kent Johnson, Marek Kapolka, Andreas Kostyrka, Roel Kramer, -Ben Last, Robert Leftwich, Stefaan Lippens, "liquider", Staffan -Malmgren, Ksenia Marasanova, JP Moins, Adam Monsen, John Nagle, "Jon", -Ed Oskiewicz, Martijn Peters, Greg Phillips, Giles Radford, Stefano -Revera, Arthur Rudolph, Marko Samastur, James Salter, Jouni Seppnen, -Alexander Schmolck, Tim Shirley, Geoffrey Sneddon, Ville Skytt, -"Vikas", Jens Svalgaard, Andy Theyers, Eric Weiser, Glyn Webster, John -Wiseman, Paul Wright, Danny Yoo - -An incomplete list of people who made suggestions or found bugs or -found ways to break Beautiful Soup: - - Hanno Bck, Matteo Bertini, Chris Curvey, Simon Cusack, Bruce Eckel, - Matt Ernst, Michael Foord, Tom Harris, Bill de hOra, Donald Howes, - Matt Patterson, Scott Roberts, Steve Strassmann, Mike Williams, - warchild at redho dot com, Sami Kuisma, Carlos Rocha, Bob Hutchison, - Joren Mc, Michal Migurski, John Kleven, Tim Heaney, Tripp Lilley, Ed - Summers, Dennis Sutch, Chris Smith, Aaron Swartz, Stuart - Turner, Greg Edwards, Kevin J Kalupson, Nikos Kouremenos, Artur de - Sousa Rocha, Yichun Wei, Per Vognsen diff --git a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/LICENSE b/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/LICENSE deleted file mode 100644 index 08e3a9c..0000000 --- a/venv/lib/python3.12/site-packages/beautifulsoup4-4.13.3.dist-info/licenses/LICENSE +++ /dev/null @@ -1,31 +0,0 @@ -Beautiful Soup is made available under the MIT license: - - Copyright (c) Leonard Richardson - - 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. - -Beautiful Soup incorporates code from the html5lib library, which is -also made available under the MIT license. Copyright (c) James Graham -and other contributors - -Beautiful Soup has an optional dependency on the soupsieve library, -which is also made available under the MIT license. Copyright (c) -Isaac Muse diff --git a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/INSTALLER deleted file mode 100644 index a1b589e..0000000 --- a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/METADATA b/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/METADATA deleted file mode 100644 index 0eaeb1d..0000000 --- a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/METADATA +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 2.1 -Name: bs4 -Version: 0.0.2 -Summary: Dummy package for Beautiful Soup (beautifulsoup4) -Author-email: Leonard Richardson -License: MIT License -Requires-Dist: beautifulsoup4 -Description-Content-Type: text/x-rst - -This is a dummy package designed to prevent namesquatting on PyPI. You should install `beautifulsoup4 `_ instead. diff --git a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/RECORD b/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/RECORD deleted file mode 100644 index 3655e41..0000000 --- a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/RECORD +++ /dev/null @@ -1,6 +0,0 @@ -README.rst,sha256=KMs4D-t40JC-oge8vGS3O5gueksurGqAIFxPtHZAMXQ,159 -bs4-0.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -bs4-0.0.2.dist-info/METADATA,sha256=GEwOSFCOYLu11XQR3O2dMO7ZTpKFZpGoIUG0gkFVgA8,411 -bs4-0.0.2.dist-info/RECORD,, -bs4-0.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -bs4-0.0.2.dist-info/WHEEL,sha256=VYAwk8D_V6zmIA2XKK-k7Fem_KAtVk3hugaRru3yjGc,105 diff --git a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/REQUESTED b/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/REQUESTED deleted file mode 100644 index e69de29..0000000 diff --git a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/WHEEL b/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/WHEEL deleted file mode 100644 index a5543ba..0000000 --- a/venv/lib/python3.12/site-packages/bs4-0.0.2.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: hatchling 1.21.0 -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any diff --git a/venv/lib/python3.12/site-packages/bs4/__init__.py b/venv/lib/python3.12/site-packages/bs4/__init__.py deleted file mode 100644 index 68a992a..0000000 --- a/venv/lib/python3.12/site-packages/bs4/__init__.py +++ /dev/null @@ -1,1170 +0,0 @@ -"""Beautiful Soup Elixir and Tonic - "The Screen-Scraper's Friend". - -http://www.crummy.com/software/BeautifulSoup/ - -Beautiful Soup uses a pluggable XML or HTML parser to parse a -(possibly invalid) document into a tree representation. Beautiful Soup -provides methods and Pythonic idioms that make it easy to navigate, -search, and modify the parse tree. - -Beautiful Soup works with Python 3.7 and up. It works better if lxml -and/or html5lib is installed, but they are not required. - -For more than you ever wanted to know about Beautiful Soup, see the -documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/ -""" - -__author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.13.3" -__copyright__ = "Copyright (c) 2004-2025 Leonard Richardson" -# Use of this source code is governed by the MIT license. -__license__ = "MIT" - -__all__ = [ - "AttributeResemblesVariableWarning", - "BeautifulSoup", - "Comment", - "Declaration", - "ProcessingInstruction", - "ResultSet", - "CSS", - "Script", - "Stylesheet", - "Tag", - "TemplateString", - "ElementFilter", - "UnicodeDammit", - "CData", - "Doctype", - - # Exceptions - "FeatureNotFound", - "ParserRejectedMarkup", - "StopParsing", - - # Warnings - "AttributeResemblesVariableWarning", - "GuessedAtParserWarning", - "MarkupResemblesLocatorWarning", - "UnusualUsageWarning", - "XMLParsedAsHTMLWarning", -] - -from collections import Counter -import sys -import warnings - -# The very first thing we do is give a useful error if someone is -# running this code under Python 2. -if sys.version_info.major < 3: - raise ImportError( - "You are trying to use a Python 3-specific version of Beautiful Soup under Python 2. This will not work. The final version of Beautiful Soup to support Python 2 was 4.9.3." - ) - -from .builder import ( - builder_registry, - TreeBuilder, -) -from .builder._htmlparser import HTMLParserTreeBuilder -from .dammit import UnicodeDammit -from .css import CSS -from ._deprecation import ( - _deprecated, -) -from .element import ( - CData, - Comment, - DEFAULT_OUTPUT_ENCODING, - Declaration, - Doctype, - NavigableString, - PageElement, - ProcessingInstruction, - PYTHON_SPECIFIC_ENCODINGS, - ResultSet, - Script, - Stylesheet, - Tag, - TemplateString, -) -from .formatter import Formatter -from .filter import ( - ElementFilter, - SoupStrainer, -) -from typing import ( - Any, - cast, - Counter as CounterType, - Dict, - Iterator, - List, - Sequence, - Optional, - Type, - Union, -) - -from bs4._typing import ( - _Encoding, - _Encodings, - _IncomingMarkup, - _InsertableElement, - _RawAttributeValue, - _RawAttributeValues, - _RawMarkup, -) - -# Import all warnings and exceptions into the main package. -from bs4.exceptions import ( - FeatureNotFound, - ParserRejectedMarkup, - StopParsing, -) -from bs4._warnings import ( - AttributeResemblesVariableWarning, - GuessedAtParserWarning, - MarkupResemblesLocatorWarning, - UnusualUsageWarning, - XMLParsedAsHTMLWarning, -) - - -class BeautifulSoup(Tag): - """A data structure representing a parsed HTML or XML document. - - Most of the methods you'll call on a BeautifulSoup object are inherited from - PageElement or Tag. - - Internally, this class defines the basic interface called by the - tree builders when converting an HTML/XML document into a data - structure. The interface abstracts away the differences between - parsers. To write a new tree builder, you'll need to understand - these methods as a whole. - - These methods will be called by the BeautifulSoup constructor: - * reset() - * feed(markup) - - The tree builder may call these methods from its feed() implementation: - * handle_starttag(name, attrs) # See note about return value - * handle_endtag(name) - * handle_data(data) # Appends to the current data node - * endData(containerClass) # Ends the current data node - - No matter how complicated the underlying parser is, you should be - able to build a tree using 'start tag' events, 'end tag' events, - 'data' events, and "done with data" events. - - If you encounter an empty-element tag (aka a self-closing tag, - like HTML's
tag), call handle_starttag and then - handle_endtag. - """ - - #: Since `BeautifulSoup` subclasses `Tag`, it's possible to treat it as - #: a `Tag` with a `Tag.name`. Hoever, this name makes it clear the - #: `BeautifulSoup` object isn't a real markup tag. - ROOT_TAG_NAME: str = "[document]" - - #: If the end-user gives no indication which tree builder they - #: want, look for one with these features. - DEFAULT_BUILDER_FEATURES: Sequence[str] = ["html", "fast"] - - #: A string containing all ASCII whitespace characters, used in - #: during parsing to detect data chunks that seem 'empty'. - ASCII_SPACES: str = "\x20\x0a\x09\x0c\x0d" - - # FUTURE PYTHON: - element_classes: Dict[Type[PageElement], Type[PageElement]] #: :meta private: - builder: TreeBuilder #: :meta private: - is_xml: bool - known_xml: Optional[bool] - parse_only: Optional[SoupStrainer] #: :meta private: - - # These members are only used while parsing markup. - markup: Optional[_RawMarkup] #: :meta private: - current_data: List[str] #: :meta private: - currentTag: Optional[Tag] #: :meta private: - tagStack: List[Tag] #: :meta private: - open_tag_counter: CounterType[str] #: :meta private: - preserve_whitespace_tag_stack: List[Tag] #: :meta private: - string_container_stack: List[Tag] #: :meta private: - _most_recent_element: Optional[PageElement] #: :meta private: - - #: Beautiful Soup's best guess as to the character encoding of the - #: original document. - original_encoding: Optional[_Encoding] - - #: The character encoding, if any, that was explicitly defined - #: in the original document. This may or may not match - #: `BeautifulSoup.original_encoding`. - declared_html_encoding: Optional[_Encoding] - - #: This is True if the markup that was parsed contains - #: U+FFFD REPLACEMENT_CHARACTER characters which were not present - #: in the original markup. These mark character sequences that - #: could not be represented in Unicode. - contains_replacement_characters: bool - - def __init__( - self, - markup: _IncomingMarkup = "", - features: Optional[Union[str, Sequence[str]]] = None, - builder: Optional[Union[TreeBuilder, Type[TreeBuilder]]] = None, - parse_only: Optional[SoupStrainer] = None, - from_encoding: Optional[_Encoding] = None, - exclude_encodings: Optional[_Encodings] = None, - element_classes: Optional[Dict[Type[PageElement], Type[PageElement]]] = None, - **kwargs: Any, - ): - """Constructor. - - :param markup: A string or a file-like object representing - markup to be parsed. - - :param features: Desirable features of the parser to be - used. This may be the name of a specific parser ("lxml", - "lxml-xml", "html.parser", or "html5lib") or it may be the - type of markup to be used ("html", "html5", "xml"). It's - recommended that you name a specific parser, so that - Beautiful Soup gives you the same results across platforms - and virtual environments. - - :param builder: A TreeBuilder subclass to instantiate (or - instance to use) instead of looking one up based on - `features`. You only need to use this if you've implemented a - custom TreeBuilder. - - :param parse_only: A SoupStrainer. Only parts of the document - matching the SoupStrainer will be considered. This is useful - when parsing part of a document that would otherwise be too - large to fit into memory. - - :param from_encoding: A string indicating the encoding of the - document to be parsed. Pass this in if Beautiful Soup is - guessing wrongly about the document's encoding. - - :param exclude_encodings: A list of strings indicating - encodings known to be wrong. Pass this in if you don't know - the document's encoding but you know Beautiful Soup's guess is - wrong. - - :param element_classes: A dictionary mapping BeautifulSoup - classes like Tag and NavigableString, to other classes you'd - like to be instantiated instead as the parse tree is - built. This is useful for subclassing Tag or NavigableString - to modify default behavior. - - :param kwargs: For backwards compatibility purposes, the - constructor accepts certain keyword arguments used in - Beautiful Soup 3. None of these arguments do anything in - Beautiful Soup 4; they will result in a warning and then be - ignored. - - Apart from this, any keyword arguments passed into the - BeautifulSoup constructor are propagated to the TreeBuilder - constructor. This makes it possible to configure a - TreeBuilder by passing in arguments, not just by saying which - one to use. - """ - if "convertEntities" in kwargs: - del kwargs["convertEntities"] - warnings.warn( - "BS4 does not respect the convertEntities argument to the " - "BeautifulSoup constructor. Entities are always converted " - "to Unicode characters." - ) - - if "markupMassage" in kwargs: - del kwargs["markupMassage"] - warnings.warn( - "BS4 does not respect the markupMassage argument to the " - "BeautifulSoup constructor. The tree builder is responsible " - "for any necessary markup massage." - ) - - if "smartQuotesTo" in kwargs: - del kwargs["smartQuotesTo"] - warnings.warn( - "BS4 does not respect the smartQuotesTo argument to the " - "BeautifulSoup constructor. Smart quotes are always converted " - "to Unicode characters." - ) - - if "selfClosingTags" in kwargs: - del kwargs["selfClosingTags"] - warnings.warn( - "Beautiful Soup 4 does not respect the selfClosingTags argument to the " - "BeautifulSoup constructor. The tree builder is responsible " - "for understanding self-closing tags." - ) - - if "isHTML" in kwargs: - del kwargs["isHTML"] - warnings.warn( - "Beautiful Soup 4 does not respect the isHTML argument to the " - "BeautifulSoup constructor. Suggest you use " - "features='lxml' for HTML and features='lxml-xml' for " - "XML." - ) - - def deprecated_argument(old_name: str, new_name: str) -> Optional[Any]: - if old_name in kwargs: - warnings.warn( - 'The "%s" argument to the BeautifulSoup constructor ' - 'was renamed to "%s" in Beautiful Soup 4.0.0' - % (old_name, new_name), - DeprecationWarning, - stacklevel=3, - ) - return kwargs.pop(old_name) - return None - - parse_only = parse_only or deprecated_argument("parseOnlyThese", "parse_only") - if parse_only is not None: - # Issue a warning if we can tell in advance that - # parse_only will exclude the entire tree. - if parse_only.excludes_everything: - warnings.warn( - f"The given value for parse_only will exclude everything: {parse_only}", - UserWarning, - stacklevel=3, - ) - - from_encoding = from_encoding or deprecated_argument( - "fromEncoding", "from_encoding" - ) - - if from_encoding and isinstance(markup, str): - warnings.warn( - "You provided Unicode markup but also provided a value for from_encoding. Your from_encoding will be ignored." - ) - from_encoding = None - - self.element_classes = element_classes or dict() - - # We need this information to track whether or not the builder - # was specified well enough that we can omit the 'you need to - # specify a parser' warning. - original_builder = builder - original_features = features - - builder_class: Type[TreeBuilder] - if isinstance(builder, type): - # A builder class was passed in; it needs to be instantiated. - builder_class = builder - builder = None - elif builder is None: - if isinstance(features, str): - features = [features] - if features is None or len(features) == 0: - features = self.DEFAULT_BUILDER_FEATURES - possible_builder_class = builder_registry.lookup(*features) - if possible_builder_class is None: - raise FeatureNotFound( - "Couldn't find a tree builder with the features you " - "requested: %s. Do you need to install a parser library?" - % ",".join(features) - ) - builder_class = possible_builder_class - - # At this point either we have a TreeBuilder instance in - # builder, or we have a builder_class that we can instantiate - # with the remaining **kwargs. - if builder is None: - builder = builder_class(**kwargs) - if ( - not original_builder - and not ( - original_features == builder.NAME - or ( - isinstance(original_features, str) - and original_features in builder.ALTERNATE_NAMES - ) - ) - and markup - ): - # The user did not tell us which TreeBuilder to use, - # and we had to guess. Issue a warning. - if builder.is_xml: - markup_type = "XML" - else: - markup_type = "HTML" - - # This code adapted from warnings.py so that we get the same line - # of code as our warnings.warn() call gets, even if the answer is wrong - # (as it may be in a multithreading situation). - caller = None - try: - caller = sys._getframe(1) - except ValueError: - pass - if caller: - globals = caller.f_globals - line_number = caller.f_lineno - else: - globals = sys.__dict__ - line_number = 1 - filename = globals.get("__file__") - if filename: - fnl = filename.lower() - if fnl.endswith((".pyc", ".pyo")): - filename = filename[:-1] - if filename: - # If there is no filename at all, the user is most likely in a REPL, - # and the warning is not necessary. - values = dict( - filename=filename, - line_number=line_number, - parser=builder.NAME, - markup_type=markup_type, - ) - warnings.warn( - GuessedAtParserWarning.MESSAGE % values, - GuessedAtParserWarning, - stacklevel=2, - ) - else: - if kwargs: - warnings.warn( - "Keyword arguments to the BeautifulSoup constructor will be ignored. These would normally be passed into the TreeBuilder constructor, but a TreeBuilder instance was passed in as `builder`." - ) - - self.builder = builder - self.is_xml = builder.is_xml - self.known_xml = self.is_xml - self._namespaces = dict() - self.parse_only = parse_only - - if hasattr(markup, "read"): # It's a file-type object. - markup = markup.read() - elif not isinstance(markup, (bytes, str)) and not hasattr(markup, "__len__"): - raise TypeError( - f"Incoming markup is of an invalid type: {markup!r}. Markup must be a string, a bytestring, or an open filehandle." - ) - elif len(markup) <= 256 and ( - (isinstance(markup, bytes) and b"<" not in markup and b"\n" not in markup) - or (isinstance(markup, str) and "<" not in markup and "\n" not in markup) - ): - # Issue warnings for a couple beginner problems - # involving passing non-markup to Beautiful Soup. - # Beautiful Soup will still parse the input as markup, - # since that is sometimes the intended behavior. - if not self._markup_is_url(markup): - self._markup_resembles_filename(markup) - - # At this point we know markup is a string or bytestring. If - # it was a file-type object, we've read from it. - markup = cast(_RawMarkup, markup) - - rejections = [] - success = False - for ( - self.markup, - self.original_encoding, - self.declared_html_encoding, - self.contains_replacement_characters, - ) in self.builder.prepare_markup( - markup, from_encoding, exclude_encodings=exclude_encodings - ): - self.reset() - self.builder.initialize_soup(self) - try: - self._feed() - success = True - break - except ParserRejectedMarkup as e: - rejections.append(e) - pass - - if not success: - other_exceptions = [str(e) for e in rejections] - raise ParserRejectedMarkup( - "The markup you provided was rejected by the parser. Trying a different parser or a different encoding may help.\n\nOriginal exception(s) from parser:\n " - + "\n ".join(other_exceptions) - ) - - # Clear out the markup and remove the builder's circular - # reference to this object. - self.markup = None - self.builder.soup = None - - def copy_self(self) -> "BeautifulSoup": - """Create a new BeautifulSoup object with the same TreeBuilder, - but not associated with any markup. - - This is the first step of the deepcopy process. - """ - clone = type(self)("", None, self.builder) - - # Keep track of the encoding of the original document, - # since we won't be parsing it again. - clone.original_encoding = self.original_encoding - return clone - - def __getstate__(self) -> Dict[str, Any]: - # Frequently a tree builder can't be pickled. - d = dict(self.__dict__) - if "builder" in d and d["builder"] is not None and not self.builder.picklable: - d["builder"] = type(self.builder) - # Store the contents as a Unicode string. - d["contents"] = [] - d["markup"] = self.decode() - - # If _most_recent_element is present, it's a Tag object left - # over from initial parse. It might not be picklable and we - # don't need it. - if "_most_recent_element" in d: - del d["_most_recent_element"] - return d - - def __setstate__(self, state: Dict[str, Any]) -> None: - # If necessary, restore the TreeBuilder by looking it up. - self.__dict__ = state - if isinstance(self.builder, type): - self.builder = self.builder() - elif not self.builder: - # We don't know which builder was used to build this - # parse tree, so use a default we know is always available. - self.builder = HTMLParserTreeBuilder() - self.builder.soup = self - self.reset() - self._feed() - - @classmethod - @_deprecated( - replaced_by="nothing (private method, will be removed)", version="4.13.0" - ) - def _decode_markup(cls, markup: _RawMarkup) -> str: - """Ensure `markup` is Unicode so it's safe to send into warnings.warn. - - warnings.warn had this problem back in 2010 but fortunately - not anymore. This has not been used for a long time; I just - noticed that fact while working on 4.13.0. - """ - if isinstance(markup, bytes): - decoded = markup.decode("utf-8", "replace") - else: - decoded = markup - return decoded - - @classmethod - def _markup_is_url(cls, markup: _RawMarkup) -> bool: - """Error-handling method to raise a warning if incoming markup looks - like a URL. - - :param markup: A string of markup. - :return: Whether or not the markup resembled a URL - closely enough to justify issuing a warning. - """ - problem: bool = False - if isinstance(markup, bytes): - problem = ( - any(markup.startswith(prefix) for prefix in (b"http:", b"https:")) - and b" " not in markup - ) - elif isinstance(markup, str): - problem = ( - any(markup.startswith(prefix) for prefix in ("http:", "https:")) - and " " not in markup - ) - else: - return False - - if not problem: - return False - warnings.warn( - MarkupResemblesLocatorWarning.URL_MESSAGE % dict(what="URL"), - MarkupResemblesLocatorWarning, - stacklevel=3, - ) - return True - - @classmethod - def _markup_resembles_filename(cls, markup: _RawMarkup) -> bool: - """Error-handling method to issue a warning if incoming markup - resembles a filename. - - :param markup: A string of markup. - :return: Whether or not the markup resembled a filename - closely enough to justify issuing a warning. - """ - markup_b: bytes - - # We're only checking ASCII characters, so rather than write - # the same tests twice, convert Unicode to a bytestring and - # operate on the bytestring. - if isinstance(markup, str): - markup_b = markup.encode("utf8") - else: - markup_b = markup - - # Step 1: does it end with a common textual file extension? - filelike = False - lower = markup_b.lower() - extensions = [b".html", b".htm", b".xml", b".xhtml", b".txt"] - if any(lower.endswith(ext) for ext in extensions): - filelike = True - if not filelike: - return False - - # Step 2: it _might_ be a file, but there are a few things - # we can look for that aren't very common in filenames. - - # Characters that have special meaning to Unix shells. (< was - # excluded before this method was called.) - # - # Many of these are also reserved characters that cannot - # appear in Windows filenames. - for byte in markup_b: - if byte in b"?*#&;>$|": - return False - - # Two consecutive forward slashes (as seen in a URL) or two - # consecutive spaces (as seen in fixed-width data). - # - # (Paths to Windows network shares contain consecutive - # backslashes, so checking that doesn't seem as helpful.) - if b"//" in markup_b: - return False - if b" " in markup_b: - return False - - # A colon in any position other than position 1 (e.g. after a - # Windows drive letter). - if markup_b.startswith(b":"): - return False - colon_i = markup_b.rfind(b":") - if colon_i not in (-1, 1): - return False - - # Step 3: If it survived all of those checks, it's similar - # enough to a file to justify issuing a warning. - warnings.warn( - MarkupResemblesLocatorWarning.FILENAME_MESSAGE % dict(what="filename"), - MarkupResemblesLocatorWarning, - stacklevel=3, - ) - return True - - def _feed(self) -> None: - """Internal method that parses previously set markup, creating a large - number of Tag and NavigableString objects. - """ - # Convert the document to Unicode. - self.builder.reset() - - if self.markup is not None: - self.builder.feed(self.markup) - # Close out any unfinished strings and close all the open tags. - self.endData() - while ( - self.currentTag is not None and self.currentTag.name != self.ROOT_TAG_NAME - ): - self.popTag() - - def reset(self) -> None: - """Reset this object to a state as though it had never parsed any - markup. - """ - Tag.__init__(self, self, self.builder, self.ROOT_TAG_NAME) - self.hidden = True - self.builder.reset() - self.current_data = [] - self.currentTag = None - self.tagStack = [] - self.open_tag_counter = Counter() - self.preserve_whitespace_tag_stack = [] - self.string_container_stack = [] - self._most_recent_element = None - self.pushTag(self) - - def new_tag( - self, - name: str, - namespace: Optional[str] = None, - nsprefix: Optional[str] = None, - attrs: Optional[_RawAttributeValues] = None, - sourceline: Optional[int] = None, - sourcepos: Optional[int] = None, - string: Optional[str] = None, - **kwattrs: _RawAttributeValue, - ) -> Tag: - """Create a new Tag associated with this BeautifulSoup object. - - :param name: The name of the new Tag. - :param namespace: The URI of the new Tag's XML namespace, if any. - :param prefix: The prefix for the new Tag's XML namespace, if any. - :param attrs: A dictionary of this Tag's attribute values; can - be used instead of ``kwattrs`` for attributes like 'class' - that are reserved words in Python. - :param sourceline: The line number where this tag was - (purportedly) found in its source document. - :param sourcepos: The character position within ``sourceline`` where this - tag was (purportedly) found. - :param string: String content for the new Tag, if any. - :param kwattrs: Keyword arguments for the new Tag's attribute values. - - """ - attr_container = self.builder.attribute_dict_class(**kwattrs) - if attrs is not None: - attr_container.update(attrs) - tag_class = self.element_classes.get(Tag, Tag) - - # Assume that this is either Tag or a subclass of Tag. If not, - # the user brought type-unsafety upon themselves. - tag_class = cast(Type[Tag], tag_class) - tag = tag_class( - None, - self.builder, - name, - namespace, - nsprefix, - attr_container, - sourceline=sourceline, - sourcepos=sourcepos, - ) - - if string is not None: - tag.string = string - return tag - - def string_container( - self, base_class: Optional[Type[NavigableString]] = None - ) -> Type[NavigableString]: - """Find the class that should be instantiated to hold a given kind of - string. - - This may be a built-in Beautiful Soup class or a custom class passed - in to the BeautifulSoup constructor. - """ - container = base_class or NavigableString - - # The user may want us to use some other class (hopefully a - # custom subclass) instead of the one we'd use normally. - container = cast( - Type[NavigableString], self.element_classes.get(container, container) - ) - - # On top of that, we may be inside a tag that needs a special - # container class. - if self.string_container_stack and container is NavigableString: - container = self.builder.string_containers.get( - self.string_container_stack[-1].name, container - ) - return container - - def new_string( - self, s: str, subclass: Optional[Type[NavigableString]] = None - ) -> NavigableString: - """Create a new `NavigableString` associated with this `BeautifulSoup` - object. - - :param s: The string content of the `NavigableString` - :param subclass: The subclass of `NavigableString`, if any, to - use. If a document is being processed, an appropriate - subclass for the current location in the document will - be determined automatically. - """ - container = self.string_container(subclass) - return container(s) - - def insert_before(self, *args: _InsertableElement) -> List[PageElement]: - """This method is part of the PageElement API, but `BeautifulSoup` doesn't implement - it because there is nothing before or after it in the parse tree. - """ - raise NotImplementedError( - "BeautifulSoup objects don't support insert_before()." - ) - - def insert_after(self, *args: _InsertableElement) -> List[PageElement]: - """This method is part of the PageElement API, but `BeautifulSoup` doesn't implement - it because there is nothing before or after it in the parse tree. - """ - raise NotImplementedError("BeautifulSoup objects don't support insert_after().") - - def popTag(self) -> Optional[Tag]: - """Internal method called by _popToTag when a tag is closed. - - :meta private: - """ - if not self.tagStack: - # Nothing to pop. This shouldn't happen. - return None - tag = self.tagStack.pop() - if tag.name in self.open_tag_counter: - self.open_tag_counter[tag.name] -= 1 - if ( - self.preserve_whitespace_tag_stack - and tag == self.preserve_whitespace_tag_stack[-1] - ): - self.preserve_whitespace_tag_stack.pop() - if self.string_container_stack and tag == self.string_container_stack[-1]: - self.string_container_stack.pop() - # print("Pop", tag.name) - if self.tagStack: - self.currentTag = self.tagStack[-1] - return self.currentTag - - def pushTag(self, tag: Tag) -> None: - """Internal method called by handle_starttag when a tag is opened. - - :meta private: - """ - # print("Push", tag.name) - if self.currentTag is not None: - self.currentTag.contents.append(tag) - self.tagStack.append(tag) - self.currentTag = self.tagStack[-1] - if tag.name != self.ROOT_TAG_NAME: - self.open_tag_counter[tag.name] += 1 - if tag.name in self.builder.preserve_whitespace_tags: - self.preserve_whitespace_tag_stack.append(tag) - if tag.name in self.builder.string_containers: - self.string_container_stack.append(tag) - - def endData(self, containerClass: Optional[Type[NavigableString]] = None) -> None: - """Method called by the TreeBuilder when the end of a data segment - occurs. - - :param containerClass: The class to use when incorporating the - data segment into the parse tree. - - :meta private: - """ - if self.current_data: - current_data = "".join(self.current_data) - # If whitespace is not preserved, and this string contains - # nothing but ASCII spaces, replace it with a single space - # or newline. - if not self.preserve_whitespace_tag_stack: - strippable = True - for i in current_data: - if i not in self.ASCII_SPACES: - strippable = False - break - if strippable: - if "\n" in current_data: - current_data = "\n" - else: - current_data = " " - - # Reset the data collector. - self.current_data = [] - - # Should we add this string to the tree at all? - if ( - self.parse_only - and len(self.tagStack) <= 1 - and (not self.parse_only.allow_string_creation(current_data)) - ): - return - - containerClass = self.string_container(containerClass) - o = containerClass(current_data) - self.object_was_parsed(o) - - def object_was_parsed( - self, - o: PageElement, - parent: Optional[Tag] = None, - most_recent_element: Optional[PageElement] = None, - ) -> None: - """Method called by the TreeBuilder to integrate an object into the - parse tree. - - :meta private: - """ - if parent is None: - parent = self.currentTag - assert parent is not None - previous_element: Optional[PageElement] - if most_recent_element is not None: - previous_element = most_recent_element - else: - previous_element = self._most_recent_element - - next_element = previous_sibling = next_sibling = None - if isinstance(o, Tag): - next_element = o.next_element - next_sibling = o.next_sibling - previous_sibling = o.previous_sibling - if previous_element is None: - previous_element = o.previous_element - - fix = parent.next_element is not None - - o.setup(parent, previous_element, next_element, previous_sibling, next_sibling) - - self._most_recent_element = o - parent.contents.append(o) - - # Check if we are inserting into an already parsed node. - if fix: - self._linkage_fixer(parent) - - def _linkage_fixer(self, el: Tag) -> None: - """Make sure linkage of this fragment is sound.""" - - first = el.contents[0] - child = el.contents[-1] - descendant: PageElement = child - - if child is first and el.parent is not None: - # Parent should be linked to first child - el.next_element = child - # We are no longer linked to whatever this element is - prev_el = child.previous_element - if prev_el is not None and prev_el is not el: - prev_el.next_element = None - # First child should be linked to the parent, and no previous siblings. - child.previous_element = el - child.previous_sibling = None - - # We have no sibling as we've been appended as the last. - child.next_sibling = None - - # This index is a tag, dig deeper for a "last descendant" - if isinstance(child, Tag) and child.contents: - # _last_decendant is typed as returning Optional[PageElement], - # but the value can't be None here, because el is a Tag - # which we know has contents. - descendant = cast(PageElement, child._last_descendant(False)) - - # As the final step, link last descendant. It should be linked - # to the parent's next sibling (if found), else walk up the chain - # and find a parent with a sibling. It should have no next sibling. - descendant.next_element = None - descendant.next_sibling = None - - target: Optional[Tag] = el - while True: - if target is None: - break - elif target.next_sibling is not None: - descendant.next_element = target.next_sibling - target.next_sibling.previous_element = child - break - target = target.parent - - def _popToTag( - self, name: str, nsprefix: Optional[str] = None, inclusivePop: bool = True - ) -> Optional[Tag]: - """Pops the tag stack up to and including the most recent - instance of the given tag. - - If there are no open tags with the given name, nothing will be - popped. - - :param name: Pop up to the most recent tag with this name. - :param nsprefix: The namespace prefix that goes with `name`. - :param inclusivePop: It this is false, pops the tag stack up - to but *not* including the most recent instqance of the - given tag. - - :meta private: - """ - # print("Popping to %s" % name) - if name == self.ROOT_TAG_NAME: - # The BeautifulSoup object itself can never be popped. - return None - - most_recently_popped = None - - stack_size = len(self.tagStack) - for i in range(stack_size - 1, 0, -1): - if not self.open_tag_counter.get(name): - break - t = self.tagStack[i] - if name == t.name and nsprefix == t.prefix: - if inclusivePop: - most_recently_popped = self.popTag() - break - most_recently_popped = self.popTag() - - return most_recently_popped - - def handle_starttag( - self, - name: str, - namespace: Optional[str], - nsprefix: Optional[str], - attrs: _RawAttributeValues, - sourceline: Optional[int] = None, - sourcepos: Optional[int] = None, - namespaces: Optional[Dict[str, str]] = None, - ) -> Optional[Tag]: - """Called by the tree builder when a new tag is encountered. - - :param name: Name of the tag. - :param nsprefix: Namespace prefix for the tag. - :param attrs: A dictionary of attribute values. Note that - attribute values are expected to be simple strings; processing - of multi-valued attributes such as "class" comes later. - :param sourceline: The line number where this tag was found in its - source document. - :param sourcepos: The character position within `sourceline` where this - tag was found. - :param namespaces: A dictionary of all namespace prefix mappings - currently in scope in the document. - - If this method returns None, the tag was rejected by an active - `ElementFilter`. You should proceed as if the tag had not occurred - in the document. For instance, if this was a self-closing tag, - don't call handle_endtag. - - :meta private: - """ - # print("Start tag %s: %s" % (name, attrs)) - self.endData() - - if ( - self.parse_only - and len(self.tagStack) <= 1 - and not self.parse_only.allow_tag_creation(nsprefix, name, attrs) - ): - return None - - tag_class = self.element_classes.get(Tag, Tag) - # Assume that this is either Tag or a subclass of Tag. If not, - # the user brought type-unsafety upon themselves. - tag_class = cast(Type[Tag], tag_class) - tag = tag_class( - self, - self.builder, - name, - namespace, - nsprefix, - attrs, - self.currentTag, - self._most_recent_element, - sourceline=sourceline, - sourcepos=sourcepos, - namespaces=namespaces, - ) - if tag is None: - return tag - if self._most_recent_element is not None: - self._most_recent_element.next_element = tag - self._most_recent_element = tag - self.pushTag(tag) - return tag - - def handle_endtag(self, name: str, nsprefix: Optional[str] = None) -> None: - """Called by the tree builder when an ending tag is encountered. - - :param name: Name of the tag. - :param nsprefix: Namespace prefix for the tag. - - :meta private: - """ - # print("End tag: " + name) - self.endData() - self._popToTag(name, nsprefix) - - def handle_data(self, data: str) -> None: - """Called by the tree builder when a chunk of textual data is - encountered. - - :meta private: - """ - self.current_data.append(data) - - def decode( - self, - indent_level: Optional[int] = None, - eventual_encoding: _Encoding = DEFAULT_OUTPUT_ENCODING, - formatter: Union[Formatter, str] = "minimal", - iterator: Optional[Iterator[PageElement]] = None, - **kwargs: Any, - ) -> str: - """Returns a string representation of the parse tree - as a full HTML or XML document. - - :param indent_level: Each line of the rendering will be - indented this many levels. (The ``formatter`` decides what a - 'level' means, in terms of spaces or other characters - output.) This is used internally in recursive calls while - pretty-printing. - :param eventual_encoding: The encoding of the final document. - If this is None, the document will be a Unicode string. - :param formatter: Either a `Formatter` object, or a string naming one of - the standard formatters. - :param iterator: The iterator to use when navigating over the - parse tree. This is only used by `Tag.decode_contents` and - you probably won't need to use it. - """ - if self.is_xml: - # Print the XML declaration - encoding_part = "" - declared_encoding: Optional[str] = eventual_encoding - if eventual_encoding in PYTHON_SPECIFIC_ENCODINGS: - # This is a special Python encoding; it can't actually - # go into an XML document because it means nothing - # outside of Python. - declared_encoding = None - if declared_encoding is not None: - encoding_part = ' encoding="%s"' % declared_encoding - prefix = '\n' % encoding_part - else: - prefix = "" - - # Prior to 4.13.0, the first argument to this method was a - # bool called pretty_print, which gave the method a different - # signature from its superclass implementation, Tag.decode. - # - # The signatures of the two methods now match, but just in - # case someone is still passing a boolean in as the first - # argument to this method (or a keyword argument with the old - # name), we can handle it and put out a DeprecationWarning. - warning: Optional[str] = None - if isinstance(indent_level, bool): - if indent_level is True: - indent_level = 0 - elif indent_level is False: - indent_level = None - warning = f"As of 4.13.0, the first argument to BeautifulSoup.decode has been changed from bool to int, to match Tag.decode. Pass in a value of {indent_level} instead." - else: - pretty_print = kwargs.pop("pretty_print", None) - assert not kwargs - if pretty_print is not None: - if pretty_print is True: - indent_level = 0 - elif pretty_print is False: - indent_level = None - warning = f"As of 4.13.0, the pretty_print argument to BeautifulSoup.decode has been removed, to match Tag.decode. Pass in a value of indent_level={indent_level} instead." - - if warning: - warnings.warn(warning, DeprecationWarning, stacklevel=2) - elif indent_level is False or pretty_print is False: - indent_level = None - return prefix + super(BeautifulSoup, self).decode( - indent_level, eventual_encoding, formatter, iterator - ) - - -# Aliases to make it easier to get started quickly, e.g. 'from bs4 import _soup' -_s = BeautifulSoup -_soup = BeautifulSoup - - -class BeautifulStoneSoup(BeautifulSoup): - """Deprecated interface to an XML parser.""" - - def __init__(self, *args: Any, **kwargs: Any): - kwargs["features"] = "xml" - warnings.warn( - "The BeautifulStoneSoup class was deprecated in version 4.0.0. Instead of using " - 'it, pass features="xml" into the BeautifulSoup constructor.', - DeprecationWarning, - stacklevel=2, - ) - super(BeautifulStoneSoup, self).__init__(*args, **kwargs) - - -# If this file is run as a script, act as an HTML pretty-printer. -if __name__ == "__main__": - import sys - - soup = BeautifulSoup(sys.stdin) - print((soup.prettify())) diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index ea695a8..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/_deprecation.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/_deprecation.cpython-312.pyc deleted file mode 100644 index 5f5931d..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/_deprecation.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/_typing.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/_typing.cpython-312.pyc deleted file mode 100644 index 0a9012e..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/_typing.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/_warnings.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/_warnings.cpython-312.pyc deleted file mode 100644 index 2779d23..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/_warnings.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/css.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/css.cpython-312.pyc deleted file mode 100644 index 42c50d2..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/css.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/dammit.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/dammit.cpython-312.pyc deleted file mode 100644 index e7bad4b..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/dammit.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/diagnose.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/diagnose.cpython-312.pyc deleted file mode 100644 index 58c273c..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/diagnose.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/element.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/element.cpython-312.pyc deleted file mode 100644 index 5b46089..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/element.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/exceptions.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/exceptions.cpython-312.pyc deleted file mode 100644 index fc6ef7b..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/exceptions.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/filter.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/filter.cpython-312.pyc deleted file mode 100644 index 436f123..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/filter.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/__pycache__/formatter.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/__pycache__/formatter.cpython-312.pyc deleted file mode 100644 index f58495f..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/__pycache__/formatter.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/_deprecation.py b/venv/lib/python3.12/site-packages/bs4/_deprecation.py deleted file mode 100644 index a0d7fdc..0000000 --- a/venv/lib/python3.12/site-packages/bs4/_deprecation.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Helper functions for deprecation. - -This interface is itself unstable and may change without warning. Do -not use these functions yourself, even as a joke. The underscores are -there for a reason. No support will be given. - -In particular, most of this will go away without warning once -Beautiful Soup drops support for Python 3.11, since Python 3.12 -defines a `@typing.deprecated() -decorator. `_ -""" - -import functools -import warnings - -from typing import ( - Any, - Callable, -) - - -def _deprecated_alias(old_name: str, new_name: str, version: str): - """Alias one attribute name to another for backward compatibility - - :meta private: - """ - - @property - def alias(self) -> Any: - ":meta private:" - warnings.warn( - f"Access to deprecated property {old_name}. (Replaced by {new_name}) -- Deprecated since version {version}.", - DeprecationWarning, - stacklevel=2, - ) - return getattr(self, new_name) - - @alias.setter - def alias(self, value: str) -> None: - ":meta private:" - warnings.warn( - f"Write to deprecated property {old_name}. (Replaced by {new_name}) -- Deprecated since version {version}.", - DeprecationWarning, - stacklevel=2, - ) - return setattr(self, new_name, value) - - return alias - - -def _deprecated_function_alias( - old_name: str, new_name: str, version: str -) -> Callable[[Any], Any]: - def alias(self, *args: Any, **kwargs: Any) -> Any: - ":meta private:" - warnings.warn( - f"Call to deprecated method {old_name}. (Replaced by {new_name}) -- Deprecated since version {version}.", - DeprecationWarning, - stacklevel=2, - ) - return getattr(self, new_name)(*args, **kwargs) - - return alias - - -def _deprecated(replaced_by: str, version: str) -> Callable: - def deprecate(func: Callable) -> Callable: - @functools.wraps(func) - def with_warning(*args: Any, **kwargs: Any) -> Any: - ":meta private:" - warnings.warn( - f"Call to deprecated method {func.__name__}. (Replaced by {replaced_by}) -- Deprecated since version {version}.", - DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return with_warning - - return deprecate diff --git a/venv/lib/python3.12/site-packages/bs4/_typing.py b/venv/lib/python3.12/site-packages/bs4/_typing.py deleted file mode 100644 index ac4ec34..0000000 --- a/venv/lib/python3.12/site-packages/bs4/_typing.py +++ /dev/null @@ -1,196 +0,0 @@ -# Custom type aliases used throughout Beautiful Soup to improve readability. - -# Notes on improvements to the type system in newer versions of Python -# that can be used once Beautiful Soup drops support for older -# versions: -# -# * ClassVar can be put on class variables now. -# * In 3.10, x|y is an accepted shorthand for Union[x,y]. -# * In 3.10, TypeAlias gains capabilities that can be used to -# improve the tree matching types (I don't remember what, exactly). -# * In 3.9 it's possible to specialize the re.Match type, -# e.g. re.Match[str]. In 3.8 there's a typing.re namespace for this, -# but it's removed in 3.12, so to support the widest possible set of -# versions I'm not using it. - -from typing_extensions import ( - runtime_checkable, - Protocol, - TypeAlias, -) -from typing import ( - Any, - Callable, - Dict, - IO, - Iterable, - Mapping, - Optional, - Pattern, - TYPE_CHECKING, - Union, -) - -if TYPE_CHECKING: - from bs4.element import ( - AttributeValueList, - NamespacedAttribute, - NavigableString, - PageElement, - ResultSet, - Tag, - ) - - -@runtime_checkable -class _RegularExpressionProtocol(Protocol): - """A protocol object which can accept either Python's built-in - `re.Pattern` objects, or the similar ``Regex`` objects defined by the - third-party ``regex`` package. - """ - - def search( - self, string: str, pos: int = ..., endpos: int = ... - ) -> Optional[Any]: ... - - @property - def pattern(self) -> str: ... - - -# Aliases for markup in various stages of processing. -# -#: The rawest form of markup: either a string, bytestring, or an open filehandle. -_IncomingMarkup: TypeAlias = Union[str, bytes, IO[str], IO[bytes]] - -#: Markup that is in memory but has (potentially) yet to be converted -#: to Unicode. -_RawMarkup: TypeAlias = Union[str, bytes] - -# Aliases for character encodings -# - -#: A data encoding. -_Encoding: TypeAlias = str - -#: One or more data encodings. -_Encodings: TypeAlias = Iterable[_Encoding] - -# Aliases for XML namespaces -# - -#: The prefix for an XML namespace. -_NamespacePrefix: TypeAlias = str - -#: The URL of an XML namespace -_NamespaceURL: TypeAlias = str - -#: A mapping of prefixes to namespace URLs. -_NamespaceMapping: TypeAlias = Dict[_NamespacePrefix, _NamespaceURL] - -#: A mapping of namespace URLs to prefixes -_InvertedNamespaceMapping: TypeAlias = Dict[_NamespaceURL, _NamespacePrefix] - -# Aliases for the attribute values associated with HTML/XML tags. -# - -#: The value associated with an HTML or XML attribute. This is the -#: relatively unprocessed value Beautiful Soup expects to come from a -#: `TreeBuilder`. -_RawAttributeValue: TypeAlias = str - -#: A dictionary of names to `_RawAttributeValue` objects. This is how -#: Beautiful Soup expects a `TreeBuilder` to represent a tag's -#: attribute values. -_RawAttributeValues: TypeAlias = ( - "Mapping[Union[str, NamespacedAttribute], _RawAttributeValue]" -) - -#: An attribute value in its final form, as stored in the -# `Tag` class, after it has been processed and (in some cases) -# split into a list of strings. -_AttributeValue: TypeAlias = Union[str, "AttributeValueList"] - -#: A dictionary of names to :py:data:`_AttributeValue` objects. This is what -#: a tag's attributes look like after processing. -_AttributeValues: TypeAlias = Dict[str, _AttributeValue] - -#: The methods that deal with turning :py:data:`_RawAttributeValue` into -#: :py:data:`_AttributeValue` may be called several times, even after the values -#: are already processed (e.g. when cloning a tag), so they need to -#: be able to acommodate both possibilities. -_RawOrProcessedAttributeValues: TypeAlias = Union[_RawAttributeValues, _AttributeValues] - -#: A number of tree manipulation methods can take either a `PageElement` or a -#: normal Python string (which will be converted to a `NavigableString`). -_InsertableElement: TypeAlias = Union["PageElement", str] - -# Aliases to represent the many possibilities for matching bits of a -# parse tree. -# -# This is very complicated because we're applying a formal type system -# to some very DWIM code. The types we end up with will be the types -# of the arguments to the SoupStrainer constructor and (more -# familiarly to Beautiful Soup users) the find* methods. - -#: A function that takes a PageElement and returns a yes-or-no answer. -_PageElementMatchFunction: TypeAlias = Callable[["PageElement"], bool] - -#: A function that takes the raw parsed ingredients of a markup tag -#: and returns a yes-or-no answer. -# Not necessary at the moment. -# _AllowTagCreationFunction:TypeAlias = Callable[[Optional[str], str, Optional[_RawAttributeValues]], bool] - -#: A function that takes the raw parsed ingredients of a markup string node -#: and returns a yes-or-no answer. -# Not necessary at the moment. -# _AllowStringCreationFunction:TypeAlias = Callable[[Optional[str]], bool] - -#: A function that takes a `Tag` and returns a yes-or-no answer. -#: A `TagNameMatchRule` expects this kind of function, if you're -#: going to pass it a function. -_TagMatchFunction: TypeAlias = Callable[["Tag"], bool] - -#: A function that takes a single string and returns a yes-or-no -#: answer. An `AttributeValueMatchRule` expects this kind of function, if -#: you're going to pass it a function. So does a `StringMatchRule`. -_StringMatchFunction: TypeAlias = Callable[[str], bool] - -#: Either a tag name, an attribute value or a string can be matched -#: against a string, bytestring, regular expression, or a boolean. -_BaseStrainable: TypeAlias = Union[str, bytes, Pattern[str], bool] - -#: A tag can be matched either with the `_BaseStrainable` options, or -#: using a function that takes the `Tag` as its sole argument. -_BaseStrainableElement: TypeAlias = Union[_BaseStrainable, _TagMatchFunction] - -#: A tag's attribute vgalue can be matched either with the -#: `_BaseStrainable` options, or using a function that takes that -#: value as its sole argument. -_BaseStrainableAttribute: TypeAlias = Union[_BaseStrainable, _StringMatchFunction] - -#: A tag can be matched using either a single criterion or a list of -#: criteria. -_StrainableElement: TypeAlias = Union[ - _BaseStrainableElement, Iterable[_BaseStrainableElement] -] - -#: An attribute value can be matched using either a single criterion -#: or a list of criteria. -_StrainableAttribute: TypeAlias = Union[ - _BaseStrainableAttribute, Iterable[_BaseStrainableAttribute] -] - -#: An string can be matched using the same techniques as -#: an attribute value. -_StrainableString: TypeAlias = _StrainableAttribute - -#: A dictionary may be used to match against multiple attribute vlaues at once. -_StrainableAttributes: TypeAlias = Dict[str, _StrainableAttribute] - -#: Many Beautiful soup methods return a PageElement or an ResultSet of -#: PageElements. A PageElement is either a Tag or a NavigableString. -#: These convenience aliases make it easier for IDE users to see which methods -#: are available on the objects they're dealing with. -_OneElement: TypeAlias = Union["PageElement", "Tag", "NavigableString"] -_AtMostOneElement: TypeAlias = Optional[_OneElement] -_QueryResults: TypeAlias = "ResultSet[_OneElement]" diff --git a/venv/lib/python3.12/site-packages/bs4/_warnings.py b/venv/lib/python3.12/site-packages/bs4/_warnings.py deleted file mode 100644 index 4309473..0000000 --- a/venv/lib/python3.12/site-packages/bs4/_warnings.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Define some custom warnings.""" - - -class GuessedAtParserWarning(UserWarning): - """The warning issued when BeautifulSoup has to guess what parser to - use -- probably because no parser was specified in the constructor. - """ - - MESSAGE: str = """No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system ("%(parser)s"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently. - -The code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, pass the additional argument 'features="%(parser)s"' to the BeautifulSoup constructor. -""" - - -class UnusualUsageWarning(UserWarning): - """A superclass for warnings issued when Beautiful Soup sees - something that is typically the result of a mistake in the calling - code, but might be intentional on the part of the user. If it is - in fact intentional, you can filter the individual warning class - to get rid of the warning. If you don't like Beautiful Soup - second-guessing what you are doing, you can filter the - UnusualUsageWarningclass itself and get rid of these entirely. - """ - - -class MarkupResemblesLocatorWarning(UnusualUsageWarning): - """The warning issued when BeautifulSoup is given 'markup' that - actually looks like a resource locator -- a URL or a path to a file - on disk. - """ - - #: :meta private: - GENERIC_MESSAGE: str = """ - -However, if you want to parse some data that happens to look like a %(what)s, then nothing has gone wrong: you are using Beautiful Soup correctly, and this warning is spurious and can be filtered. To make this warning go away, run this code before calling the BeautifulSoup constructor: - - from bs4 import MarkupResemblesLocatorWarning - import warnings - - warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) - """ - - URL_MESSAGE: str = ( - """The input passed in on this line looks more like a URL than HTML or XML. - -If you meant to use Beautiful Soup to parse the web page found at a certain URL, then something has gone wrong. You should use an Python package like 'requests' to fetch the content behind the URL. Once you have the content as a string, you can feed that string into Beautiful Soup.""" - + GENERIC_MESSAGE - ) - - FILENAME_MESSAGE: str = ( - """The input passed in on this line looks more like a filename than HTML or XML. - -If you meant to use Beautiful Soup to parse the contents of a file on disk, then something has gone wrong. You should open the file first, using code like this: - - filehandle = open(your filename) - -You can then feed the open filehandle into Beautiful Soup instead of using the filename.""" - + GENERIC_MESSAGE - ) - - -class AttributeResemblesVariableWarning(UnusualUsageWarning, SyntaxWarning): - """The warning issued when Beautiful Soup suspects a provided - attribute name may actually be the misspelled name of a Beautiful - Soup variable. Generally speaking, this is only used in cases like - "_class" where it's very unlikely the user would be referencing an - XML attribute with that name. - """ - - MESSAGE: str = """%(original)r is an unusual attribute name and is a common misspelling for %(autocorrect)r. - -If you meant %(autocorrect)r, change your code to use it, and this warning will go away. - -If you really did mean to check the %(original)r attribute, this warning is spurious and can be filtered. To make it go away, run this code before creating your BeautifulSoup object: - - from bs4 import AttributeResemblesVariableWarning - import warnings - - warnings.filterwarnings("ignore", category=AttributeResemblesVariableWarning) -""" - - -class XMLParsedAsHTMLWarning(UnusualUsageWarning): - """The warning issued when an HTML parser is used to parse - XML that is not (as far as we can tell) XHTML. - """ - - MESSAGE: str = """It looks like you're using an HTML parser to parse an XML document. - -Assuming this really is an XML document, what you're doing might work, but you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the Python package 'lxml' installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor. - -If you want or need to use an HTML parser on this document, you can make this warning go away by filtering it. To do that, run this code before calling the BeautifulSoup constructor: - - from bs4 import XMLParsedAsHTMLWarning - import warnings - - warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning) -""" diff --git a/venv/lib/python3.12/site-packages/bs4/builder/__init__.py b/venv/lib/python3.12/site-packages/bs4/builder/__init__.py deleted file mode 100644 index 5f2b38d..0000000 --- a/venv/lib/python3.12/site-packages/bs4/builder/__init__.py +++ /dev/null @@ -1,848 +0,0 @@ -from __future__ import annotations - -# Use of this source code is governed by the MIT license. -__license__ = "MIT" - -from collections import defaultdict -import re -from types import ModuleType -from typing import ( - Any, - cast, - Dict, - Iterable, - List, - Optional, - Pattern, - Set, - Tuple, - Type, - TYPE_CHECKING, -) -import warnings -import sys -from bs4.element import ( - AttributeDict, - AttributeValueList, - CharsetMetaAttributeValue, - ContentMetaAttributeValue, - RubyParenthesisString, - RubyTextString, - Stylesheet, - Script, - TemplateString, - nonwhitespace_re, -) - -# Exceptions were moved to their own module in 4.13. Import here for -# backwards compatibility. -from bs4.exceptions import ParserRejectedMarkup - -from bs4._typing import ( - _AttributeValues, - _RawAttributeValue, -) - -from bs4._warnings import XMLParsedAsHTMLWarning - -if TYPE_CHECKING: - from bs4 import BeautifulSoup - from bs4.element import ( - NavigableString, - Tag, - ) - from bs4._typing import ( - _AttributeValue, - _Encoding, - _Encodings, - _RawOrProcessedAttributeValues, - _RawMarkup, - ) - -__all__ = [ - "HTMLTreeBuilder", - "SAXTreeBuilder", - "TreeBuilder", - "TreeBuilderRegistry", -] - -# Some useful features for a TreeBuilder to have. -FAST = "fast" -PERMISSIVE = "permissive" -STRICT = "strict" -XML = "xml" -HTML = "html" -HTML_5 = "html5" - -__all__ = [ - "TreeBuilderRegistry", - "TreeBuilder", - "HTMLTreeBuilder", - "DetectsXMLParsedAsHTML", - - "ParserRejectedMarkup", # backwards compatibility only as of 4.13.0 -] - -class TreeBuilderRegistry(object): - """A way of looking up TreeBuilder subclasses by their name or by desired - features. - """ - - builders_for_feature: Dict[str, List[Type[TreeBuilder]]] - builders: List[Type[TreeBuilder]] - - def __init__(self) -> None: - self.builders_for_feature = defaultdict(list) - self.builders = [] - - def register(self, treebuilder_class: type[TreeBuilder]) -> None: - """Register a treebuilder based on its advertised features. - - :param treebuilder_class: A subclass of `TreeBuilder`. its - `TreeBuilder.features` attribute should list its features. - """ - for feature in treebuilder_class.features: - self.builders_for_feature[feature].insert(0, treebuilder_class) - self.builders.insert(0, treebuilder_class) - - def lookup(self, *features: str) -> Optional[Type[TreeBuilder]]: - """Look up a TreeBuilder subclass with the desired features. - - :param features: A list of features to look for. If none are - provided, the most recently registered TreeBuilder subclass - will be used. - :return: A TreeBuilder subclass, or None if there's no - registered subclass with all the requested features. - """ - if len(self.builders) == 0: - # There are no builders at all. - return None - - if len(features) == 0: - # They didn't ask for any features. Give them the most - # recently registered builder. - return self.builders[0] - - # Go down the list of features in order, and eliminate any builders - # that don't match every feature. - feature_list = list(features) - feature_list.reverse() - candidates = None - candidate_set = None - while len(feature_list) > 0: - feature = feature_list.pop() - we_have_the_feature = self.builders_for_feature.get(feature, []) - if len(we_have_the_feature) > 0: - if candidates is None: - candidates = we_have_the_feature - candidate_set = set(candidates) - else: - # Eliminate any candidates that don't have this feature. - candidate_set = candidate_set.intersection(set(we_have_the_feature)) - - # The only valid candidates are the ones in candidate_set. - # Go through the original list of candidates and pick the first one - # that's in candidate_set. - if candidate_set is None or candidates is None: - return None - for candidate in candidates: - if candidate in candidate_set: - return candidate - return None - - -#: The `BeautifulSoup` constructor will take a list of features -#: and use it to look up `TreeBuilder` classes in this registry. -builder_registry: TreeBuilderRegistry = TreeBuilderRegistry() - - -class TreeBuilder(object): - """Turn a textual document into a Beautiful Soup object tree. - - This is an abstract superclass which smooths out the behavior of - different parser libraries into a single, unified interface. - - :param multi_valued_attributes: If this is set to None, the - TreeBuilder will not turn any values for attributes like - 'class' into lists. Setting this to a dictionary will - customize this behavior; look at :py:attr:`bs4.builder.HTMLTreeBuilder.DEFAULT_CDATA_LIST_ATTRIBUTES` - for an example. - - Internally, these are called "CDATA list attributes", but that - probably doesn't make sense to an end-user, so the argument name - is ``multi_valued_attributes``. - - :param preserve_whitespace_tags: A set of tags to treat - the way
 tags are treated in HTML. Tags in this set
-     are immune from pretty-printing; their contents will always be
-     output as-is.
-
-    :param string_containers: A dictionary mapping tag names to
-     the classes that should be instantiated to contain the textual
-     contents of those tags. The default is to use NavigableString
-     for every tag, no matter what the name. You can override the
-     default by changing :py:attr:`DEFAULT_STRING_CONTAINERS`.
-
-    :param store_line_numbers: If the parser keeps track of the line
-     numbers and positions of the original markup, that information
-     will, by default, be stored in each corresponding
-     :py:class:`bs4.element.Tag` object. You can turn this off by
-     passing store_line_numbers=False; then Tag.sourcepos and
-     Tag.sourceline will always be None. If the parser you're using
-     doesn't keep track of this information, then store_line_numbers
-     is irrelevant.
-
-    :param attribute_dict_class: The value of a multi-valued attribute
-      (such as HTML's 'class') willl be stored in an instance of this
-      class.  The default is Beautiful Soup's built-in
-      `AttributeValueList`, which is a normal Python list, and you
-      will probably never need to change it.
-    """
-
-    USE_DEFAULT: Any = object()  #: :meta private:
-
-    def __init__(
-        self,
-        multi_valued_attributes: Dict[str, Set[str]] = USE_DEFAULT,
-        preserve_whitespace_tags: Set[str] = USE_DEFAULT,
-        store_line_numbers: bool = USE_DEFAULT,
-        string_containers: Dict[str, Type[NavigableString]] = USE_DEFAULT,
-        empty_element_tags: Set[str] = USE_DEFAULT,
-        attribute_dict_class: Type[AttributeDict] = AttributeDict,
-        attribute_value_list_class: Type[AttributeValueList] = AttributeValueList,
-    ):
-        self.soup = None
-        if multi_valued_attributes is self.USE_DEFAULT:
-            multi_valued_attributes = self.DEFAULT_CDATA_LIST_ATTRIBUTES
-        self.cdata_list_attributes = multi_valued_attributes
-        if preserve_whitespace_tags is self.USE_DEFAULT:
-            preserve_whitespace_tags = self.DEFAULT_PRESERVE_WHITESPACE_TAGS
-        self.preserve_whitespace_tags = preserve_whitespace_tags
-        if empty_element_tags is self.USE_DEFAULT:
-            self.empty_element_tags = self.DEFAULT_EMPTY_ELEMENT_TAGS
-        else:
-            self.empty_element_tags = empty_element_tags
-        # TODO: store_line_numbers is probably irrelevant now that
-        # the behavior of sourceline and sourcepos has been made consistent
-        # everywhere.
-        if store_line_numbers == self.USE_DEFAULT:
-            store_line_numbers = self.TRACKS_LINE_NUMBERS
-        self.store_line_numbers = store_line_numbers
-        if string_containers == self.USE_DEFAULT:
-            string_containers = self.DEFAULT_STRING_CONTAINERS
-        self.string_containers = string_containers
-        self.attribute_dict_class = attribute_dict_class
-        self.attribute_value_list_class = attribute_value_list_class
-
-    NAME: str = "[Unknown tree builder]"
-    ALTERNATE_NAMES: Iterable[str] = []
-    features: Iterable[str] = []
-
-    is_xml: bool = False
-    picklable: bool = False
-
-    soup: Optional[BeautifulSoup]  #: :meta private:
-
-    #: A tag will be considered an empty-element
-    #: tag when and only when it has no contents.
-    empty_element_tags: Optional[Set[str]] = None  #: :meta private:
-    cdata_list_attributes: Dict[str, Set[str]]  #: :meta private:
-    preserve_whitespace_tags: Set[str]  #: :meta private:
-    string_containers: Dict[str, Type[NavigableString]]  #: :meta private:
-    tracks_line_numbers: bool  #: :meta private:
-
-    #: A value for these tag/attribute combinations is a space- or
-    #: comma-separated list of CDATA, rather than a single CDATA.
-    DEFAULT_CDATA_LIST_ATTRIBUTES: Dict[str, Set[str]] = defaultdict(set)
-
-    #: Whitespace should be preserved inside these tags.
-    DEFAULT_PRESERVE_WHITESPACE_TAGS: Set[str] = set()
-
-    #: The textual contents of tags with these names should be
-    #: instantiated with some class other than `bs4.element.NavigableString`.
-    DEFAULT_STRING_CONTAINERS: Dict[str, Type[bs4.element.NavigableString]] = {}
-
-    #: By default, tags are treated as empty-element tags if they have
-    #: no contents--that is, using XML rules. HTMLTreeBuilder
-    #: defines a different set of DEFAULT_EMPTY_ELEMENT_TAGS based on the
-    #: HTML 4 and HTML5 standards.
-    DEFAULT_EMPTY_ELEMENT_TAGS: Optional[Set[str]] = None
-
-    #: Most parsers don't keep track of line numbers.
-    TRACKS_LINE_NUMBERS: bool = False
-
-    def initialize_soup(self, soup: BeautifulSoup) -> None:
-        """The BeautifulSoup object has been initialized and is now
-        being associated with the TreeBuilder.
-
-        :param soup: A BeautifulSoup object.
-        """
-        self.soup = soup
-
-    def reset(self) -> None:
-        """Do any work necessary to reset the underlying parser
-        for a new document.
-
-        By default, this does nothing.
-        """
-        pass
-
-    def can_be_empty_element(self, tag_name: str) -> bool:
-        """Might a tag with this name be an empty-element tag?
-
-        The final markup may or may not actually present this tag as
-        self-closing.
-
-        For instance: an HTMLBuilder does not consider a 

tag to be - an empty-element tag (it's not in - HTMLBuilder.empty_element_tags). This means an empty

tag - will be presented as "

", not "

" or "

". - - The default implementation has no opinion about which tags are - empty-element tags, so a tag will be presented as an - empty-element tag if and only if it has no children. - "" will become "", and "bar" will - be left alone. - - :param tag_name: The name of a markup tag. - """ - if self.empty_element_tags is None: - return True - return tag_name in self.empty_element_tags - - def feed(self, markup: _RawMarkup) -> None: - """Run incoming markup through some parsing process.""" - raise NotImplementedError() - - def prepare_markup( - self, - markup: _RawMarkup, - user_specified_encoding: Optional[_Encoding] = None, - document_declared_encoding: Optional[_Encoding] = None, - exclude_encodings: Optional[_Encodings] = None, - ) -> Iterable[Tuple[_RawMarkup, Optional[_Encoding], Optional[_Encoding], bool]]: - """Run any preliminary steps necessary to make incoming markup - acceptable to the parser. - - :param markup: The markup that's about to be parsed. - :param user_specified_encoding: The user asked to try this encoding - to convert the markup into a Unicode string. - :param document_declared_encoding: The markup itself claims to be - in this encoding. NOTE: This argument is not used by the - calling code and can probably be removed. - :param exclude_encodings: The user asked *not* to try any of - these encodings. - - :yield: A series of 4-tuples: (markup, encoding, declared encoding, - has undergone character replacement) - - Each 4-tuple represents a strategy that the parser can try - to convert the document to Unicode and parse it. Each - strategy will be tried in turn. - - By default, the only strategy is to parse the markup - as-is. See `LXMLTreeBuilderForXML` and - `HTMLParserTreeBuilder` for implementations that take into - account the quirks of particular parsers. - - :meta private: - - """ - yield markup, None, None, False - - def test_fragment_to_document(self, fragment: str) -> str: - """Wrap an HTML fragment to make it look like a document. - - Different parsers do this differently. For instance, lxml - introduces an empty tag, and html5lib - doesn't. Abstracting this away lets us write simple tests - which run HTML fragments through the parser and compare the - results against other HTML fragments. - - This method should not be used outside of unit tests. - - :param fragment: A fragment of HTML. - :return: A full HTML document. - :meta private: - """ - return fragment - - def set_up_substitutions(self, tag: Tag) -> bool: - """Set up any substitutions that will need to be performed on - a `Tag` when it's output as a string. - - By default, this does nothing. See `HTMLTreeBuilder` for a - case where this is used. - - :return: Whether or not a substitution was performed. - :meta private: - """ - return False - - def _replace_cdata_list_attribute_values( - self, tag_name: str, attrs: _RawOrProcessedAttributeValues - ) -> _AttributeValues: - """When an attribute value is associated with a tag that can - have multiple values for that attribute, convert the string - value to a list of strings. - - Basically, replaces class="foo bar" with class=["foo", "bar"] - - NOTE: This method modifies its input in place. - - :param tag_name: The name of a tag. - :param attrs: A dictionary containing the tag's attributes. - Any appropriate attribute values will be modified in place. - :return: The modified dictionary that was originally passed in. - """ - - # First, cast the attrs dict to _AttributeValues. This might - # not be accurate yet, but it will be by the time this method - # returns. - modified_attrs = cast(_AttributeValues, attrs) - if not modified_attrs or not self.cdata_list_attributes: - # Nothing to do. - return modified_attrs - - # There is at least a possibility that we need to modify one of - # the attribute values. - universal: Set[str] = self.cdata_list_attributes.get("*", set()) - tag_specific = self.cdata_list_attributes.get(tag_name.lower(), None) - for attr in list(modified_attrs.keys()): - modified_value: _AttributeValue - if attr in universal or (tag_specific and attr in tag_specific): - # We have a "class"-type attribute whose string - # value is a whitespace-separated list of - # values. Split it into a list. - original_value: _AttributeValue = modified_attrs[attr] - if isinstance(original_value, _RawAttributeValue): - # This is a _RawAttributeValue (a string) that - # needs to be split and converted to a - # AttributeValueList so it can be an - # _AttributeValue. - modified_value = self.attribute_value_list_class( - nonwhitespace_re.findall(original_value) - ) - else: - # html5lib calls setAttributes twice for the - # same tag when rearranging the parse tree. On - # the second call the attribute value here is - # already a list. This can also happen when a - # Tag object is cloned. If this happens, leave - # the value alone rather than trying to split - # it again. - modified_value = original_value - modified_attrs[attr] = modified_value - return modified_attrs - - -class SAXTreeBuilder(TreeBuilder): - """A Beautiful Soup treebuilder that listens for SAX events. - - This is not currently used for anything, and it will be removed - soon. It was a good idea, but it wasn't properly integrated into the - rest of Beautiful Soup, so there have been long stretches where it - hasn't worked properly. - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( - "The SAXTreeBuilder class was deprecated in 4.13.0 and will be removed soon thereafter. It is completely untested and probably doesn't work; do not use it.", - DeprecationWarning, - stacklevel=2, - ) - super(SAXTreeBuilder, self).__init__(*args, **kwargs) - - def feed(self, markup: _RawMarkup) -> None: - raise NotImplementedError() - - def close(self) -> None: - pass - - def startElement(self, name: str, attrs: Dict[str, str]) -> None: - attrs = AttributeDict((key[1], value) for key, value in list(attrs.items())) - # print("Start %s, %r" % (name, attrs)) - assert self.soup is not None - self.soup.handle_starttag(name, None, None, attrs) - - def endElement(self, name: str) -> None: - # print("End %s" % name) - assert self.soup is not None - self.soup.handle_endtag(name) - - def startElementNS( - self, nsTuple: Tuple[str, str], nodeName: str, attrs: Dict[str, str] - ) -> None: - # Throw away (ns, nodeName) for now. - self.startElement(nodeName, attrs) - - def endElementNS(self, nsTuple: Tuple[str, str], nodeName: str) -> None: - # Throw away (ns, nodeName) for now. - self.endElement(nodeName) - # handler.endElementNS((ns, node.nodeName), node.nodeName) - - def startPrefixMapping(self, prefix: str, nodeValue: str) -> None: - # Ignore the prefix for now. - pass - - def endPrefixMapping(self, prefix: str) -> None: - # Ignore the prefix for now. - # handler.endPrefixMapping(prefix) - pass - - def characters(self, content: str) -> None: - assert self.soup is not None - self.soup.handle_data(content) - - def startDocument(self) -> None: - pass - - def endDocument(self) -> None: - pass - - -class HTMLTreeBuilder(TreeBuilder): - """This TreeBuilder knows facts about HTML, such as which tags are treated - specially by the HTML standard. - """ - - #: Some HTML tags are defined as having no contents. Beautiful Soup - #: treats these specially. - DEFAULT_EMPTY_ELEMENT_TAGS: Set[str] = set( - [ - # These are from HTML5. - "area", - "base", - "br", - "col", - "embed", - "hr", - "img", - "input", - "keygen", - "link", - "menuitem", - "meta", - "param", - "source", - "track", - "wbr", - # These are from earlier versions of HTML and are removed in HTML5. - "basefont", - "bgsound", - "command", - "frame", - "image", - "isindex", - "nextid", - "spacer", - ] - ) - - #: The HTML standard defines these tags as block-level elements. Beautiful - #: Soup does not treat these elements differently from other elements, - #: but it may do so eventually, and this information is available if - #: you need to use it. - DEFAULT_BLOCK_ELEMENTS: Set[str] = set( - [ - "address", - "article", - "aside", - "blockquote", - "canvas", - "dd", - "div", - "dl", - "dt", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "header", - "hr", - "li", - "main", - "nav", - "noscript", - "ol", - "output", - "p", - "pre", - "section", - "table", - "tfoot", - "ul", - "video", - ] - ) - - #: These HTML tags need special treatment so they can be - #: represented by a string class other than `bs4.element.NavigableString`. - #: - #: For some of these tags, it's because the HTML standard defines - #: an unusual content model for them. I made this list by going - #: through the HTML spec - #: (https://html.spec.whatwg.org/#metadata-content) and looking for - #: "metadata content" elements that can contain strings. - #: - #: The Ruby tags ( and ) are here despite being normal - #: "phrasing content" tags, because the content they contain is - #: qualitatively different from other text in the document, and it - #: can be useful to be able to distinguish it. - #: - #: TODO: Arguably

foo

" - soup = self.soup(markup) - return doctype.encode("utf8"), soup - - def test_normal_doctypes(self): - """Make sure normal, everyday HTML doctypes are handled correctly.""" - self.assertDoctypeHandled("html") - self.assertDoctypeHandled( - 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' - ) - - def test_empty_doctype(self): - soup = self.soup("") - doctype = soup.contents[0] - assert "" == doctype.strip() - - def test_mixed_case_doctype(self): - # A lowercase or mixed-case doctype becomes a Doctype. - for doctype_fragment in ("doctype", "DocType"): - doctype_str, soup = self._document_with_doctype("html", doctype_fragment) - - # Make sure a Doctype object was created and that the DOCTYPE - # is uppercase. - doctype = soup.contents[0] - assert doctype.__class__ == Doctype - assert doctype == "html" - assert soup.encode("utf8")[: len(doctype_str)] == b"" - - # Make sure that the doctype was correctly associated with the - # parse tree and that the rest of the document parsed. - assert soup.p.contents[0] == "foo" - - def test_public_doctype_with_url(self): - doctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"' - self.assertDoctypeHandled(doctype) - - def test_system_doctype(self): - self.assertDoctypeHandled('foo SYSTEM "http://www.example.com/"') - - def test_namespaced_system_doctype(self): - # We can handle a namespaced doctype with a system ID. - self.assertDoctypeHandled('xsl:stylesheet SYSTEM "htmlent.dtd"') - - def test_namespaced_public_doctype(self): - # Test a namespaced doctype with a public id. - self.assertDoctypeHandled('xsl:stylesheet PUBLIC "htmlent.dtd"') - - def test_real_xhtml_document(self): - """A real XHTML document should come out more or less the same as it went in.""" - markup = b""" - - -Hello. -Goodbye. -""" - with warnings.catch_warnings(record=True) as w: - soup = self.soup(markup) - assert soup.encode("utf-8").replace(b"\n", b"") == markup.replace(b"\n", b"") - - # No warning was issued about parsing an XML document as HTML, - # because XHTML is both. - assert w == [] - - def test_namespaced_html(self): - # When a namespaced XML document is parsed as HTML it should - # be treated as HTML with weird tag names. - markup = b"""content""" - with warnings.catch_warnings(record=True) as w: - soup = self.soup(markup) - - assert 2 == len(soup.find_all("ns1:foo")) - - # n.b. no "you're parsing XML as HTML" warning was given - # because there was no XML declaration. - assert [] == w - - def test_detect_xml_parsed_as_html(self): - # A warning is issued when parsing an XML document as HTML, - # but basic stuff should still work. - markup = b"""string""" - with warnings.catch_warnings(record=True) as w: - soup = self.soup(markup) - assert soup.tag.string == "string" - [warning] = w - assert isinstance(warning.message, XMLParsedAsHTMLWarning) - assert str(warning.message) == XMLParsedAsHTMLWarning.MESSAGE - - # NOTE: the warning is not issued if the document appears to - # be XHTML (tested with test_real_xhtml_document in the - # superclass) or if there is no XML declaration (tested with - # test_namespaced_html in the superclass). - - def test_processing_instruction(self): - # We test both Unicode and bytestring to verify that - # process_markup correctly sets processing_instruction_class - # even when the markup is already Unicode and there is no - # need to process anything. - markup = """""" - soup = self.soup(markup) - assert markup == soup.decode() - - markup = b"""""" - soup = self.soup(markup) - assert markup == soup.encode("utf8") - - def test_deepcopy(self): - """Make sure you can copy the tree builder. - - This is important because the builder is part of a - BeautifulSoup object, and we want to be able to copy that. - """ - copy.deepcopy(self.default_builder) - - def test_p_tag_is_never_empty_element(self): - """A

tag is never designated as an empty-element tag. - - Even if the markup shows it as an empty-element tag, it - shouldn't be presented that way. - """ - soup = self.soup("

") - assert not soup.p.is_empty_element - assert str(soup.p) == "

" - - def test_unclosed_tags_get_closed(self): - """A tag that's not closed by the end of the document should be closed. - - This applies to all tags except empty-element tags. - """ - self.assert_soup("

", "

") - self.assert_soup("", "") - - self.assert_soup("
", "
") - - def test_br_is_always_empty_element_tag(self): - """A
tag is designated as an empty-element tag. - - Some parsers treat

as one
tag, some parsers as - two tags, but it should always be an empty-element tag. - """ - soup = self.soup("

") - assert soup.br.is_empty_element - assert str(soup.br) == "
" - - def test_nested_formatting_elements(self): - self.assert_soup("") - - def test_double_head(self): - html = """ - - -Ordinary HEAD element test - - - -Hello, world! - - -""" - soup = self.soup(html) - assert "text/javascript" == soup.find("script")["type"] - - def test_comment(self): - # Comments are represented as Comment objects. - markup = "

foobaz

" - self.assert_soup(markup) - - soup = self.soup(markup) - comment = soup.find(string="foobar") - assert comment.__class__ == Comment - - # The comment is properly integrated into the tree. - foo = soup.find(string="foo") - assert comment == foo.next_element - baz = soup.find(string="baz") - assert comment == baz.previous_element - - def test_preserved_whitespace_in_pre_and_textarea(self): - """Whitespace must be preserved in
 and \n"
-        self.assert_soup(pre_markup)
-        self.assert_soup(textarea_markup)
-
-        soup = self.soup(pre_markup)
-        assert soup.pre.prettify() == pre_markup
-
-        soup = self.soup(textarea_markup)
-        assert soup.textarea.prettify() == textarea_markup
-
-        soup = self.soup("")
-        assert soup.textarea.prettify() == "\n"
-
-    def test_nested_inline_elements(self):
-        """Inline elements can be nested indefinitely."""
-        b_tag = "Inside a B tag"
-        self.assert_soup(b_tag)
-
-        nested_b_tag = "

A nested tag

" - self.assert_soup(nested_b_tag) - - double_nested_b_tag = "

A doubly nested tag

" - self.assert_soup(double_nested_b_tag) - - def test_nested_block_level_elements(self): - """Block elements can be nested.""" - soup = self.soup("

Foo

") - blockquote = soup.blockquote - assert blockquote.p.b.string == "Foo" - assert blockquote.b.string == "Foo" - - def test_correctly_nested_tables(self): - """One table can go inside another one.""" - markup = ( - '' - "" - "" - ) - - self.assert_soup( - markup, - '
Here's another table:" - '' - "" - "
foo
Here\'s another table:' - '
foo
' - "
", - ) - - self.assert_soup( - "" - "" - "
Foo
Bar
Baz
" - ) - - def test_multivalued_attribute_with_whitespace(self): - # Whitespace separating the values of a multi-valued attribute - # should be ignored. - - markup = '
' - soup = self.soup(markup) - assert ["foo", "bar"] == soup.div["class"] - - # If you search by the literal name of the class it's like the whitespace - # wasn't there. - assert soup.div == soup.find("div", class_="foo bar") - - def test_deeply_nested_multivalued_attribute(self): - # html5lib can set the attributes of the same tag many times - # as it rearranges the tree. This has caused problems with - # multivalued attributes. - markup = '
' - soup = self.soup(markup) - assert ["css"] == soup.div.div["class"] - - def test_multivalued_attribute_on_html(self): - # html5lib uses a different API to set the attributes ot the - # tag. This has caused problems with multivalued - # attributes. - markup = '' - soup = self.soup(markup) - assert ["a", "b"] == soup.html["class"] - - def test_angle_brackets_in_attribute_values_are_escaped(self): - self.assert_soup('', '') - - def test_strings_resembling_character_entity_references(self): - # "&T" and "&p" look like incomplete character entities, but they are - # not. - self.assert_soup( - "

• AT&T is in the s&p 500

", - "

\u2022 AT&T is in the s&p 500

", - ) - - def test_apos_entity(self): - self.assert_soup( - "

Bob's Bar

", - "

Bob's Bar

", - ) - - def test_entities_in_foreign_document_encoding(self): - # “ and ” are invalid numeric entities referencing - # Windows-1252 characters. - references a character common - # to Windows-1252 and Unicode, and ☃ references a - # character only found in Unicode. - # - # All of these entities should be converted to Unicode - # characters. - markup = "

“Hello” -☃

" - soup = self.soup(markup) - assert "“Hello” -☃" == soup.p.string - - def test_entities_in_attributes_converted_to_unicode(self): - expect = '

' - self.assert_soup('

', expect) - self.assert_soup('

', expect) - self.assert_soup('

', expect) - self.assert_soup('

', expect) - - def test_entities_in_text_converted_to_unicode(self): - expect = "

pi\N{LATIN SMALL LETTER N WITH TILDE}ata

" - self.assert_soup("

piñata

", expect) - self.assert_soup("

piñata

", expect) - self.assert_soup("

piñata

", expect) - self.assert_soup("

piñata

", expect) - - def test_quot_entity_converted_to_quotation_mark(self): - self.assert_soup( - "

I said "good day!"

", '

I said "good day!"

' - ) - - def test_out_of_range_entity(self): - expect = "\N{REPLACEMENT CHARACTER}" - self.assert_soup("�", expect) - self.assert_soup("�", expect) - self.assert_soup("�", expect) - - def test_multipart_strings(self): - "Mostly to prevent a recurrence of a bug in the html5lib treebuilder." - soup = self.soup("

\nfoo

") - assert "p" == soup.h2.string.next_element.name - assert "p" == soup.p.name - self.assertConnectedness(soup) - - def test_invalid_html_entity(self): - # The html.parser treebuilder can't distinguish between an - # invalid HTML entity with a semicolon and an invalid HTML - # entity with no semicolon (see its subclass for the tested - # behavior). But the other treebuilders can. - markup = "

a &nosuchentity b

" - soup = self.soup(markup) - assert "

a &nosuchentity b

" == soup.p.decode() - - markup = "

a &nosuchentity; b

" - soup = self.soup(markup) - assert "

a &nosuchentity; b

" == soup.p.decode() - - def test_head_tag_between_head_and_body(self): - "Prevent recurrence of a bug in the html5lib treebuilder." - content = """ - - foo - -""" - soup = self.soup(content) - assert soup.html.body is not None - self.assertConnectedness(soup) - - def test_multiple_copies_of_a_tag(self): - "Prevent recurrence of a bug in the html5lib treebuilder." - content = """ - - - - - -""" - soup = self.soup(content) - self.assertConnectedness(soup.article) - - def test_basic_namespaces(self): - """Parsers don't need to *understand* namespaces, but at the - very least they should not choke on namespaces or lose - data.""" - - markup = b'4' - soup = self.soup(markup) - assert markup == soup.encode() - assert "http://www.w3.org/1999/xhtml" == soup.html["xmlns"] - assert "http://www.w3.org/1998/Math/MathML" == soup.html["xmlns:mathml"] - assert "http://www.w3.org/2000/svg" == soup.html["xmlns:svg"] - - def test_multivalued_attribute_value_becomes_list(self): - markup = b'' - soup = self.soup(markup) - assert ["foo", "bar"] == soup.a["class"] - - # - # Generally speaking, tests below this point are more tests of - # Beautiful Soup than tests of the tree builders. But parsers are - # weird, so we run these tests separately for every tree builder - # to detect any differences between them. - # - - def test_can_parse_unicode_document(self): - # A seemingly innocuous document... but it's in Unicode! And - # it contains characters that can't be represented in the - # encoding found in the declaration! The horror! - markup = 'Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!' - soup = self.soup(markup) - assert "Sacr\xe9 bleu!" == soup.body.string - - def test_soupstrainer(self): - """Parsers should be able to work with SoupStrainers.""" - strainer = SoupStrainer("b") - soup = self.soup("A bold statement", parse_only=strainer) - assert soup.decode() == "bold" - - def test_single_quote_attribute_values_become_double_quotes(self): - self.assert_soup("", '') - - def test_attribute_values_with_nested_quotes_are_left_alone(self): - text = """a""" - self.assert_soup(text) - - def test_attribute_values_with_double_nested_quotes_get_quoted(self): - text = """a""" - soup = self.soup(text) - soup.foo["attr"] = 'Brawls happen at "Bob\'s Bar"' - self.assert_soup( - soup.foo.decode(), - """a""", - ) - - def test_ampersand_in_attribute_value_gets_escaped(self): - self.assert_soup( - '', - '', - ) - - self.assert_soup( - 'foo', - 'foo', - ) - - def test_escaped_ampersand_in_attribute_value_is_left_alone(self): - self.assert_soup('') - - def test_entities_in_strings_converted_during_parsing(self): - # Both XML and HTML entities are converted to Unicode characters - # during parsing. - text = "

<<sacré bleu!>>

" - expected = ( - "

<<sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>

" - ) - self.assert_soup(text, expected) - - def test_smart_quotes_converted_on_the_way_in(self): - # Microsoft smart quotes are converted to Unicode characters during - # parsing. - quote = b"

\x91Foo\x92

" - soup = self.soup(quote, from_encoding="windows-1252") - assert ( - soup.p.string - == "\N{LEFT SINGLE QUOTATION MARK}Foo\N{RIGHT SINGLE QUOTATION MARK}" - ) - - def test_non_breaking_spaces_converted_on_the_way_in(self): - soup = self.soup("  ") - assert soup.a.string == "\N{NO-BREAK SPACE}" * 2 - - def test_entities_converted_on_the_way_out(self): - text = "

<<sacré bleu!>>

" - expected = "

<<sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!>>

".encode( - "utf-8" - ) - soup = self.soup(text) - assert soup.p.encode("utf-8") == expected - - def test_real_iso_8859_document(self): - # Smoke test of interrelated functionality, using an - # easy-to-understand document. - - # Here it is in Unicode. Note that it claims to be in ISO-8859-1. - unicode_html = '

Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!

' - - # That's because we're going to encode it into ISO-8859-1, - # and use that to test. - iso_latin_html = unicode_html.encode("iso-8859-1") - - # Parse the ISO-8859-1 HTML. - soup = self.soup(iso_latin_html) - - # Encode it to UTF-8. - result = soup.encode("utf-8") - - # What do we expect the result to look like? Well, it would - # look like unicode_html, except that the META tag would say - # UTF-8 instead of ISO-8859-1. - expected = unicode_html.replace("ISO-8859-1", "utf-8") - - # And, of course, it would be in UTF-8, not Unicode. - expected = expected.encode("utf-8") - - # Ta-da! - assert result == expected - - def test_real_shift_jis_document(self): - # Smoke test to make sure the parser can handle a document in - # Shift-JIS encoding, without choking. - shift_jis_html = ( - b"
"
-            b"\x82\xb1\x82\xea\x82\xcdShift-JIS\x82\xc5\x83R\x81[\x83f"
-            b"\x83B\x83\x93\x83O\x82\xb3\x82\xea\x82\xbd\x93\xfa\x96{\x8c"
-            b"\xea\x82\xcc\x83t\x83@\x83C\x83\x8b\x82\xc5\x82\xb7\x81B"
-            b"
" - ) - unicode_html = shift_jis_html.decode("shift-jis") - soup = self.soup(unicode_html) - - # Make sure the parse tree is correctly encoded to various - # encodings. - assert soup.encode("utf-8") == unicode_html.encode("utf-8") - assert soup.encode("euc_jp") == unicode_html.encode("euc_jp") - - def test_real_hebrew_document(self): - # A real-world test to make sure we can convert ISO-8859-9 (a - # Hebrew encoding) to UTF-8. - hebrew_document = b"Hebrew (ISO 8859-8) in Visual Directionality

Hebrew (ISO 8859-8) in Visual Directionality

\xed\xe5\xec\xf9" - soup = self.soup(hebrew_document, from_encoding="iso8859-8") - # Some tree builders call it iso8859-8, others call it iso-8859-9. - # That's not a difference we really care about. - assert soup.original_encoding in ("iso8859-8", "iso-8859-8") - assert soup.encode("utf-8") == ( - hebrew_document.decode("iso8859-8").encode("utf-8") - ) - - def test_meta_tag_reflects_current_encoding(self): - # Here's the tag saying that a document is - # encoded in Shift-JIS. - meta_tag = ( - '' - ) - - # Here's a document incorporating that meta tag. - shift_jis_html = ( - "\n%s\n" - '' - "Shift-JIS markup goes here." - ) % meta_tag - soup = self.soup(shift_jis_html) - - # Parse the document, and the charset is seemingly unaffected. - parsed_meta = soup.find("meta", {"http-equiv": "Content-type"}) - content = parsed_meta["content"] - assert "text/html; charset=x-sjis" == content - - # But that value is actually a ContentMetaAttributeValue object. - assert isinstance(content, ContentMetaAttributeValue) - - # And it will take on a value that reflects its current - # encoding. - assert "text/html; charset=utf8" == content.substitute_encoding("utf8") - - # No matter how the tag is encoded, its charset attribute - # will always be accurate. - assert b"charset=utf8" in parsed_meta.encode("utf8") - assert b"charset=shift-jis" in parsed_meta.encode("shift-jis") - - # For the rest of the story, see TestSubstitutions in - # test_tree.py. - - def test_html5_style_meta_tag_reflects_current_encoding(self): - # Here's the tag saying that a document is - # encoded in Shift-JIS. - meta_tag = '' - - # Here's a document incorporating that meta tag. - shift_jis_html = ( - "\n%s\n" - '' - "Shift-JIS markup goes here." - ) % meta_tag - soup = self.soup(shift_jis_html) - - # Parse the document, and the charset is seemingly unaffected. - parsed_meta = soup.find("meta", id="encoding") - charset = parsed_meta["charset"] - assert "x-sjis" == charset - - # But that value is actually a CharsetMetaAttributeValue object. - assert isinstance(charset, CharsetMetaAttributeValue) - - # And it will take on a value that reflects its current - # encoding. - assert "utf8" == charset.substitute_encoding("utf8") - - # No matter how the tag is encoded, its charset attribute - # will always be accurate. - assert b'charset="utf8"' in parsed_meta.encode("utf8") - assert b'charset="shift-jis"' in parsed_meta.encode("shift-jis") - - def test_python_specific_encodings_not_used_in_charset(self): - # You can encode an HTML document using a Python-specific - # encoding, but that encoding won't be mentioned _inside_ the - # resulting document. Instead, the document will appear to - # have no encoding. - for markup in [ - b'' b'' - ]: - soup = self.soup(markup) - for encoding in PYTHON_SPECIFIC_ENCODINGS: - if encoding in ( - "idna", - "mbcs", - "oem", - "undefined", - "string_escape", - "string-escape", - ): - # For one reason or another, these will raise an - # exception if we actually try to use them, so don't - # bother. - continue - encoded = soup.encode(encoding) - assert b'meta charset=""' in encoded - assert encoding.encode("ascii") not in encoded - - def test_tag_with_no_attributes_can_have_attributes_added(self): - data = self.soup("text") - data.a["foo"] = "bar" - assert 'text' == data.a.decode() - - def test_closing_tag_with_no_opening_tag(self): - # Without BeautifulSoup.open_tag_counter, the tag will - # cause _popToTag to be called over and over again as we look - # for a tag that wasn't there. The result is that 'text2' - # will show up outside the body of the document. - soup = self.soup("

text1

text2
") - assert "

text1

text2
" == soup.body.decode() - - def test_worst_case(self): - """Test the worst case (currently) for linking issues.""" - - soup = self.soup(BAD_DOCUMENT) - self.linkage_validator(soup) - - -class XMLTreeBuilderSmokeTest(TreeBuilderSmokeTest): - def test_pickle_and_unpickle_identity(self): - # Pickling a tree, then unpickling it, yields a tree identical - # to the original. - tree = self.soup("foo") - dumped = pickle.dumps(tree, 2) - loaded = pickle.loads(dumped) - assert loaded.__class__ == BeautifulSoup - assert loaded.decode() == tree.decode() - - def test_docstring_generated(self): - soup = self.soup("") - assert soup.encode() == b'\n' - - def test_xml_declaration(self): - markup = b"""\n""" - soup = self.soup(markup) - assert markup == soup.encode("utf8") - - def test_python_specific_encodings_not_used_in_xml_declaration(self): - # You can encode an XML document using a Python-specific - # encoding, but that encoding won't be mentioned _inside_ the - # resulting document. - markup = b"""\n""" - soup = self.soup(markup) - for encoding in PYTHON_SPECIFIC_ENCODINGS: - if encoding in ( - "idna", - "mbcs", - "oem", - "undefined", - "string_escape", - "string-escape", - ): - # For one reason or another, these will raise an - # exception if we actually try to use them, so don't - # bother. - continue - encoded = soup.encode(encoding) - assert b'' in encoded - assert encoding.encode("ascii") not in encoded - - def test_processing_instruction(self): - markup = b"""\n""" - soup = self.soup(markup) - assert markup == soup.encode("utf8") - - def test_real_xhtml_document(self): - """A real XHTML document should come out *exactly* the same as it went in.""" - markup = b""" - - -Hello. -Goodbye. -""" - soup = self.soup(markup) - assert soup.encode("utf-8") == markup - - def test_nested_namespaces(self): - doc = b""" - - - - - -""" - soup = self.soup(doc) - assert doc == soup.encode() - - def test_formatter_processes_script_tag_for_xml_documents(self): - doc = """ - -""" - soup = BeautifulSoup(doc, "lxml-xml") - # lxml would have stripped this while parsing, but we can add - # it later. - soup.script.string = 'console.log("< < hey > > ");' - encoded = soup.encode() - assert b"< < hey > >" in encoded - - def test_can_parse_unicode_document(self): - markup = 'Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!' - soup = self.soup(markup) - assert "Sacr\xe9 bleu!" == soup.root.string - - def test_can_parse_unicode_document_begining_with_bom(self): - markup = '\N{BYTE ORDER MARK}Sacr\N{LATIN SMALL LETTER E WITH ACUTE} bleu!' - soup = self.soup(markup) - assert "Sacr\xe9 bleu!" == soup.root.string - - def test_popping_namespaced_tag(self): - markup = 'b2012-07-02T20:33:42Zcd' - soup = self.soup(markup) - assert str(soup.rss) == markup - - def test_docstring_includes_correct_encoding(self): - soup = self.soup("") - assert ( - soup.encode("latin1") == b'\n' - ) - - def test_large_xml_document(self): - """A large XML document should come out the same as it went in.""" - markup = ( - b'\n' - + b"0" * (2**12) - + b"" - ) - soup = self.soup(markup) - assert soup.encode("utf-8") == markup - - def test_tags_are_empty_element_if_and_only_if_they_are_empty(self): - self.assert_soup("

", "

") - self.assert_soup("

foo

") - - def test_namespaces_are_preserved(self): - markup = 'This tag is in the a namespaceThis tag is in the b namespace' - soup = self.soup(markup) - root = soup.root - assert "http://example.com/" == root["xmlns:a"] - assert "http://example.net/" == root["xmlns:b"] - - def test_closing_namespaced_tag(self): - markup = '

20010504

' - soup = self.soup(markup) - assert str(soup.p) == markup - - def test_namespaced_attributes(self): - markup = '' - soup = self.soup(markup) - assert str(soup.foo) == markup - - def test_namespaced_attributes_xml_namespace(self): - markup = 'bar' - soup = self.soup(markup) - assert str(soup.foo) == markup - - def test_find_by_prefixed_name(self): - doc = """ - - foo - bar - baz - -""" - soup = self.soup(doc) - - # There are three tags. - assert 3 == len(soup.find_all("tag")) - - # But two of them are ns1:tag and one of them is ns2:tag. - assert 2 == len(soup.find_all("ns1:tag")) - assert 1 == len(soup.find_all("ns2:tag")) - - assert 1, len(soup.find_all("ns2:tag", key="value")) - assert 3, len(soup.find_all(["ns1:tag", "ns2:tag"])) - - def test_copy_tag_preserves_namespace(self): - xml = """ -""" - - soup = self.soup(xml) - tag = soup.document - duplicate = copy.copy(tag) - - # The two tags have the same namespace prefix. - assert tag.prefix == duplicate.prefix - - def test_worst_case(self): - """Test the worst case (currently) for linking issues.""" - - soup = self.soup(BAD_DOCUMENT) - self.linkage_validator(soup) - - -class HTML5TreeBuilderSmokeTest(HTMLTreeBuilderSmokeTest): - """Smoke test for a tree builder that supports HTML5.""" - - def test_real_xhtml_document(self): - # Since XHTML is not HTML5, HTML5 parsers are not tested to handle - # XHTML documents in any particular way. - pass - - def test_html_tags_have_namespace(self): - markup = "" - soup = self.soup(markup) - assert "http://www.w3.org/1999/xhtml" == soup.a.namespace - - def test_svg_tags_have_namespace(self): - markup = "" - soup = self.soup(markup) - namespace = "http://www.w3.org/2000/svg" - assert namespace == soup.svg.namespace - assert namespace == soup.circle.namespace - - def test_mathml_tags_have_namespace(self): - markup = "5" - soup = self.soup(markup) - namespace = "http://www.w3.org/1998/Math/MathML" - assert namespace == soup.math.namespace - assert namespace == soup.msqrt.namespace - - def test_xml_declaration_becomes_comment(self): - markup = '' - soup = self.soup(markup) - assert isinstance(soup.contents[0], Comment) - assert soup.contents[0] == '?xml version="1.0" encoding="utf-8"?' - assert "html" == soup.contents[0].next_element.name diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/__init__.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8ba5c2d..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder.cpython-312.pyc deleted file mode 100644 index b988d5c..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder_registry.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder_registry.cpython-312.pyc deleted file mode 100644 index 60fa134..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_builder_registry.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_css.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_css.cpython-312.pyc deleted file mode 100644 index 635e46c..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_css.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_dammit.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_dammit.cpython-312.pyc deleted file mode 100644 index 2cb49cb..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_dammit.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_element.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_element.cpython-312.pyc deleted file mode 100644 index 28dc9a3..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_element.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_filter.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_filter.cpython-312.pyc deleted file mode 100644 index 43f98bf..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_filter.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_formatter.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_formatter.cpython-312.pyc deleted file mode 100644 index 0721388..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_formatter.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_fuzz.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_fuzz.cpython-312.pyc deleted file mode 100644 index d320c40..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_fuzz.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_html5lib.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_html5lib.cpython-312.pyc deleted file mode 100644 index 4e41695..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_html5lib.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_htmlparser.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_htmlparser.cpython-312.pyc deleted file mode 100644 index 20d9371..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_htmlparser.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_lxml.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_lxml.cpython-312.pyc deleted file mode 100644 index 35ba915..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_lxml.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_navigablestring.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_navigablestring.cpython-312.pyc deleted file mode 100644 index 437cbfe..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_navigablestring.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_pageelement.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_pageelement.cpython-312.pyc deleted file mode 100644 index 4efb4b1..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_pageelement.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_soup.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_soup.cpython-312.pyc deleted file mode 100644 index 7e58d8a..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_soup.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tag.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tag.cpython-312.pyc deleted file mode 100644 index 4226b51..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tag.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tree.cpython-312.pyc b/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tree.cpython-312.pyc deleted file mode 100644 index 93ad221..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/__pycache__/test_tree.cpython-312.pyc and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase deleted file mode 100644 index 4828f8a..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256.testcase +++ /dev/null @@ -1 +0,0 @@ -

\ No newline at end of file diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5000587759190016.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5000587759190016.testcase deleted file mode 100644 index 8a585ce..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5000587759190016.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5167584867909632.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5167584867909632.testcase deleted file mode 100644 index 0fe66dd..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5167584867909632.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5270998950477824.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5270998950477824.testcase deleted file mode 100644 index fd41142..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5270998950477824.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5375146639360000.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5375146639360000.testcase deleted file mode 100644 index 6248b2c..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5375146639360000.testcase +++ /dev/null @@ -1 +0,0 @@ - >tet>< \ No newline at end of file diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5492400320282624.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5492400320282624.testcase deleted file mode 100644 index 107da53..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5492400320282624.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5703933063462912.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5703933063462912.testcase deleted file mode 100644 index 367106c..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-5703933063462912.testcase +++ /dev/null @@ -1,2 +0,0 @@ - -t \ No newline at end of file diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6450958476902400.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6450958476902400.testcase deleted file mode 100644 index a823d55..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6450958476902400.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6600557255327744.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6600557255327744.testcase deleted file mode 100644 index 65af44d..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/clusterfuzz-testcase-minimized-bs4_fuzzer-6600557255327744.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-0d306a50c8ed8bcd0785b67000fcd5dea1d33f08.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-0d306a50c8ed8bcd0785b67000fcd5dea1d33f08.testcase deleted file mode 100644 index 5559adb..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-0d306a50c8ed8bcd0785b67000fcd5dea1d33f08.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-ffbdfa8a2b26f13537b68d3794b0478a4090ee4a.testcase b/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-ffbdfa8a2b26f13537b68d3794b0478a4090ee4a.testcase deleted file mode 100644 index 8857115..0000000 Binary files a/venv/lib/python3.12/site-packages/bs4/tests/fuzz/crash-ffbdfa8a2b26f13537b68d3794b0478a4090ee4a.testcase and /dev/null differ diff --git a/venv/lib/python3.12/site-packages/bs4/tests/test_builder.py b/venv/lib/python3.12/site-packages/bs4/tests/test_builder.py deleted file mode 100644 index 87d6758..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/test_builder.py +++ /dev/null @@ -1,28 +0,0 @@ -import pytest -from unittest.mock import patch -from bs4.builder import DetectsXMLParsedAsHTML - - -class TestDetectsXMLParsedAsHTML: - @pytest.mark.parametrize( - "markup,looks_like_xml", - [ - ("No xml declaration", False), - ("obviously HTMLActually XHTML", False), - (" < html>Tricky XHTML", False), - ("", True), - ], - ) - def test_warn_if_markup_looks_like_xml(self, markup, looks_like_xml): - # Test of our ability to guess at whether markup looks XML-ish - # _and_ not HTML-ish. - with patch("bs4.builder.DetectsXMLParsedAsHTML._warn") as mock: - for data in markup, markup.encode("utf8"): - result = DetectsXMLParsedAsHTML.warn_if_markup_looks_like_xml(data) - assert result == looks_like_xml - if looks_like_xml: - assert mock.called - else: - assert not mock.called - mock.reset_mock() diff --git a/venv/lib/python3.12/site-packages/bs4/tests/test_builder_registry.py b/venv/lib/python3.12/site-packages/bs4/tests/test_builder_registry.py deleted file mode 100644 index ad4b5a9..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/test_builder_registry.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Tests of the builder registry.""" - -import pytest -import warnings -from typing import Type - -from bs4 import BeautifulSoup -from bs4.builder import ( - builder_registry as registry, - TreeBuilder, - TreeBuilderRegistry, -) -from bs4.builder._htmlparser import HTMLParserTreeBuilder - -from . import ( - HTML5LIB_PRESENT, - LXML_PRESENT, -) - -if HTML5LIB_PRESENT: - from bs4.builder._html5lib import HTML5TreeBuilder - -if LXML_PRESENT: - from bs4.builder._lxml import ( - LXMLTreeBuilderForXML, - LXMLTreeBuilder, - ) - - -# TODO: Split out the lxml and html5lib tests into their own classes -# and gate with pytest.mark.skipIf. -class TestBuiltInRegistry(object): - """Test the built-in registry with the default builders registered.""" - - def test_combination(self): - assert registry.lookup("strict", "html") == HTMLParserTreeBuilder - if LXML_PRESENT: - assert registry.lookup("fast", "html") == LXMLTreeBuilder - assert registry.lookup("permissive", "xml") == LXMLTreeBuilderForXML - if HTML5LIB_PRESENT: - assert registry.lookup("html5lib", "html") == HTML5TreeBuilder - - def test_lookup_by_markup_type(self): - if LXML_PRESENT: - assert registry.lookup("html") == LXMLTreeBuilder - assert registry.lookup("xml") == LXMLTreeBuilderForXML - else: - assert registry.lookup("xml") is None - if HTML5LIB_PRESENT: - assert registry.lookup("html") == HTML5TreeBuilder - else: - assert registry.lookup("html") == HTMLParserTreeBuilder - - def test_named_library(self): - if LXML_PRESENT: - assert registry.lookup("lxml", "xml") == LXMLTreeBuilderForXML - assert registry.lookup("lxml", "html") == LXMLTreeBuilder - if HTML5LIB_PRESENT: - assert registry.lookup("html5lib") == HTML5TreeBuilder - - assert registry.lookup("html.parser") == HTMLParserTreeBuilder - - def test_beautifulsoup_constructor_does_lookup(self): - with warnings.catch_warnings(record=True): - # This will create a warning about not explicitly - # specifying a parser, but we'll ignore it. - - # You can pass in a string. - BeautifulSoup("", features="html") - # Or a list of strings. - BeautifulSoup("", features=["html", "fast"]) - pass - - # You'll get an exception if BS can't find an appropriate - # builder. - with pytest.raises(ValueError): - BeautifulSoup("", features="no-such-feature") - - -class TestRegistry(object): - """Test the TreeBuilderRegistry class in general.""" - - def setup_method(self): - self.registry = TreeBuilderRegistry() - - def builder_for_features(self, *feature_list: str) -> Type[TreeBuilder]: - cls = type( - "Builder_" + "_".join(feature_list), (object,), {"features": feature_list} - ) - - self.registry.register(cls) - return cls - - def test_register_with_no_features(self): - builder = self.builder_for_features() - - # Since the builder advertises no features, you can't find it - # by looking up features. - assert self.registry.lookup("foo") is None - - # But you can find it by doing a lookup with no features, if - # this happens to be the only registered builder. - assert self.registry.lookup() == builder - - def test_register_with_features_makes_lookup_succeed(self): - builder = self.builder_for_features("foo", "bar") - assert self.registry.lookup("foo") is builder - assert self.registry.lookup("bar") is builder - - def test_lookup_fails_when_no_builder_implements_feature(self): - assert self.registry.lookup("baz") is None - - def test_lookup_gets_most_recent_registration_when_no_feature_specified(self): - self.builder_for_features("foo") - builder2 = self.builder_for_features("bar") - assert self.registry.lookup() == builder2 - - def test_lookup_fails_when_no_tree_builders_registered(self): - assert self.registry.lookup() is None - - def test_lookup_gets_most_recent_builder_supporting_all_features(self): - self.builder_for_features("foo") - self.builder_for_features("bar") - has_both_early = self.builder_for_features("foo", "bar", "baz") - has_both_late = self.builder_for_features("foo", "bar", "quux") - self.builder_for_features("bar") - self.builder_for_features("foo") - - # There are two builders featuring 'foo' and 'bar', but - # the one that also features 'quux' was registered later. - assert self.registry.lookup("foo", "bar") == has_both_late - - # There is only one builder featuring 'foo', 'bar', and 'baz'. - assert self.registry.lookup("foo", "bar", "baz") == has_both_early - - def test_lookup_fails_when_cannot_reconcile_requested_features(self): - self.builder_for_features("foo", "bar") - self.builder_for_features("foo", "baz") - assert self.registry.lookup("bar", "baz") is None diff --git a/venv/lib/python3.12/site-packages/bs4/tests/test_css.py b/venv/lib/python3.12/site-packages/bs4/tests/test_css.py deleted file mode 100644 index b1c4237..0000000 --- a/venv/lib/python3.12/site-packages/bs4/tests/test_css.py +++ /dev/null @@ -1,536 +0,0 @@ -import pytest -import types - -from bs4 import ( - BeautifulSoup, - ResultSet, -) - -from typing import ( - Any, - List, - Tuple, - Type, -) - -from packaging.version import Version - -from . import ( - SoupTest, - SOUP_SIEVE_PRESENT, -) - -SOUPSIEVE_EXCEPTION_ON_UNSUPPORTED_PSEUDOCLASS: Type[Exception] -if SOUP_SIEVE_PRESENT: - from soupsieve import __version__, SelectorSyntaxError - - # Some behavior changes in soupsieve 2.6 that affects one of our - # tests. For the test to run under all versions of Python - # supported by Beautiful Soup (which includes versions of Python - # not supported by soupsieve 2.6) we need to check both behaviors. - SOUPSIEVE_EXCEPTION_ON_UNSUPPORTED_PSEUDOCLASS = SelectorSyntaxError - if Version(__version__) < Version("2.6"): - SOUPSIEVE_EXCEPTION_ON_UNSUPPORTED_PSEUDOCLASS = NotImplementedError - - -@pytest.mark.skipif(not SOUP_SIEVE_PRESENT, reason="Soup Sieve not installed") -class TestCSSSelectors(SoupTest): - """Test basic CSS selector functionality. - - This functionality is implemented in soupsieve, which has a much - more comprehensive test suite, so this is basically an extra check - that soupsieve works as expected. - """ - - HTML = """ - - - -The title - - - -Hello there. -
-
-

An H1

-

Some text

-

Some more text

-

An H2

-

Another

-Bob -

Another H2

-me - -span1a1 -span1a2 test - -span2a1 - - - -
- -
- - - - - - - - -

English

-

English UK

-

English US

-

French

-
- - -""" - - def setup_method(self): - self._soup = BeautifulSoup(self.HTML, "html.parser") - - def assert_css_selects( - self, selector: str, expected_ids: List[str], **kwargs: Any - ) -> None: - results = self._soup.select(selector, **kwargs) - assert isinstance(results, ResultSet) - el_ids = [el["id"] for el in results] - el_ids.sort() - expected_ids.sort() - assert expected_ids == el_ids, "Selector %s, expected [%s], got [%s]" % ( - selector, - ", ".join(expected_ids), - ", ".join(el_ids), - ) - - assertSelect = assert_css_selects - - def assert_css_select_multiple(self, *tests: Tuple[str, List[str]]): - for selector, expected_ids in tests: - self.assert_css_selects(selector, expected_ids) - - def test_precompiled(self): - sel = self._soup.css.compile("div") - - els = self._soup.select(sel) - assert len(els) == 4 - for div in els: - assert div.name == "div" - - el = self._soup.select_one(sel) - assert "main" == el["id"] - - def test_one_tag_one(self): - els = self._soup.select("title") - assert len(els) == 1 - assert els[0].name == "title" - assert els[0].contents == ["The title"] - - def test_one_tag_many(self): - els = self._soup.select("div") - assert len(els) == 4 - for div in els: - assert div.name == "div" - - el = self._soup.select_one("div") - assert "main" == el["id"] - - def test_select_one_returns_none_if_no_match(self): - match = self._soup.select_one("nonexistenttag") - assert None is match - - def test_tag_in_tag_one(self): - self.assert_css_selects("div div", ["inner", "data1"]) - - def test_tag_in_tag_many(self): - for selector in ("html div", "html body div", "body div"): - self.assert_css_selects(selector, ["data1", "main", "inner", "footer"]) - - def test_limit(self): - self.assert_css_selects("html div", ["main"], limit=1) - self.assert_css_selects("html body div", ["inner", "main"], limit=2) - self.assert_css_selects( - "body div", ["data1", "main", "inner", "footer"], limit=10 - ) - - def test_tag_no_match(self): - assert len(self._soup.select("del")) == 0 - - def test_invalid_tag(self): - with pytest.raises(SelectorSyntaxError): - self._soup.select("tag%t") - - def test_select_dashed_tag_ids(self): - self.assert_css_selects("custom-dashed-tag", ["dash1", "dash2"]) - - def test_select_dashed_by_id(self): - dashed = self._soup.select('custom-dashed-tag[id="dash2"]') - assert dashed[0].name == "custom-dashed-tag" - assert dashed[0]["id"] == "dash2" - - def test_dashed_tag_text(self): - assert self._soup.select("body > custom-dashed-tag")[0].text == "Hello there." - - def test_select_dashed_matches_find_all(self): - assert self._soup.select("custom-dashed-tag") == self._soup.find_all( - "custom-dashed-tag" - ) - - def test_header_tags(self): - self.assert_css_select_multiple( - ("h1", ["header1"]), - ("h2", ["header2", "header3"]), - ) - - def test_class_one(self): - for selector in (".onep", "p.onep", "html p.onep"): - els = self._soup.select(selector) - assert len(els) == 1 - assert els[0].name == "p" - assert els[0]["class"] == ["onep"] - - def test_class_mismatched_tag(self): - els = self._soup.select("div.onep") - assert len(els) == 0 - - def test_one_id(self): - for selector in ("div#inner", "#inner", "div div#inner"): - self.assert_css_selects(selector, ["inner"]) - - def test_bad_id(self): - els = self._soup.select("#doesnotexist") - assert len(els) == 0 - - def test_items_in_id(self): - els = self._soup.select("div#inner p") - assert len(els) == 3 - for el in els: - assert el.name == "p" - assert els[1]["class"] == ["onep"] - assert not els[0].has_attr("class") - - def test_a_bunch_of_emptys(self): - for selector in ("div#main del", "div#main div.oops", "div div#main"): - assert len(self._soup.select(selector)) == 0 - - def test_multi_class_support(self): - for selector in ( - ".class1", - "p.class1", - ".class2", - "p.class2", - ".class3", - "p.class3", - "html p.class2", - "div#inner .class2", - ): - self.assert_css_selects(selector, ["pmulti"]) - - def test_multi_class_selection(self): - for selector in (".class1.class3", ".class3.class2", ".class1.class2.class3"): - self.assert_css_selects(selector, ["pmulti"]) - - def test_child_selector(self): - self.assert_css_selects(".s1 > a", ["s1a1", "s1a2"]) - self.assert_css_selects(".s1 > a span", ["s1a2s1"]) - - def test_child_selector_id(self): - self.assert_css_selects(".s1 > a#s1a2 span", ["s1a2s1"]) - - def test_attribute_equals(self): - self.assert_css_select_multiple( - ('p[class="onep"]', ["p1"]), - ('p[id="p1"]', ["p1"]), - ('[class="onep"]', ["p1"]), - ('[id="p1"]', ["p1"]), - ('link[rel="stylesheet"]', ["l1"]), - ('link[type="text/css"]', ["l1"]), - ('link[href="blah.css"]', ["l1"]), - ('link[href="no-blah.css"]', []), - ('[rel="stylesheet"]', ["l1"]), - ('[type="text/css"]', ["l1"]), - ('[href="blah.css"]', ["l1"]), - ('[href="no-blah.css"]', []), - ('p[href="no-blah.css"]', []), - ('[href="no-blah.css"]', []), - ) - - def test_attribute_tilde(self): - self.assert_css_select_multiple( - ('p[class~="class1"]', ["pmulti"]), - ('p[class~="class2"]', ["pmulti"]), - ('p[class~="class3"]', ["pmulti"]), - ('[class~="class1"]', ["pmulti"]), - ('[class~="class2"]', ["pmulti"]), - ('[class~="class3"]', ["pmulti"]), - ('a[rel~="friend"]', ["bob"]), - ('a[rel~="met"]', ["bob"]), - ('[rel~="friend"]', ["bob"]), - ('[rel~="met"]', ["bob"]), - ) - - def test_attribute_startswith(self): - self.assert_css_select_multiple( - ('[rel^="style"]', ["l1"]), - ('link[rel^="style"]', ["l1"]), - ('notlink[rel^="notstyle"]', []), - ('[rel^="notstyle"]', []), - ('link[rel^="notstyle"]', []), - ('link[href^="bla"]', ["l1"]), - ('a[href^="http://"]', ["bob", "me"]), - ('[href^="http://"]', ["bob", "me"]), - ('[id^="p"]', ["pmulti", "p1"]), - ('[id^="m"]', ["me", "main"]), - ('div[id^="m"]', ["main"]), - ('a[id^="m"]', ["me"]), - ('div[data-tag^="dashed"]', ["data1"]), - ) - - def test_attribute_endswith(self): - self.assert_css_select_multiple( - ('[href$=".css"]', ["l1"]), - ('link[href$=".css"]', ["l1"]), - ('link[id$="1"]', ["l1"]), - ( - '[id$="1"]', - ["data1", "l1", "p1", "header1", "s1a1", "s2a1", "s1a2s1", "dash1"], - ), - ('div[id$="1"]', ["data1"]), - ('[id$="noending"]', []), - ) - - def test_attribute_contains(self): - self.assert_css_select_multiple( - # From test_attribute_startswith - ('[rel*="style"]', ["l1"]), - ('link[rel*="style"]', ["l1"]), - ('notlink[rel*="notstyle"]', []), - ('[rel*="notstyle"]', []), - ('link[rel*="notstyle"]', []), - ('link[href*="bla"]', ["l1"]), - ('[href*="http://"]', ["bob", "me"]), - ('[id*="p"]', ["pmulti", "p1"]), - ('div[id*="m"]', ["main"]), - ('a[id*="m"]', ["me"]), - # From test_attribute_endswith - ('[href*=".css"]', ["l1"]), - ('link[href*=".css"]', ["l1"]), - ('link[id*="1"]', ["l1"]), - ( - '[id*="1"]', - [ - "data1", - "l1", - "p1", - "header1", - "s1a1", - "s1a2", - "s2a1", - "s1a2s1", - "dash1", - ], - ), - ('div[id*="1"]', ["data1"]), - ('[id*="noending"]', []), - # New for this test - ('[href*="."]', ["bob", "me", "l1"]), - ('a[href*="."]', ["bob", "me"]), - ('link[href*="."]', ["l1"]), - ('div[id*="n"]', ["main", "inner"]), - ('div[id*="nn"]', ["inner"]), - ('div[data-tag*="edval"]', ["data1"]), - ) - - def test_attribute_exact_or_hypen(self): - self.assert_css_select_multiple( - ('p[lang|="en"]', ["lang-en", "lang-en-gb", "lang-en-us"]), - ('[lang|="en"]', ["lang-en", "lang-en-gb", "lang-en-us"]), - ('p[lang|="fr"]', ["lang-fr"]), - ('p[lang|="gb"]', []), - ) - - def test_attribute_exists(self): - self.assert_css_select_multiple( - ("[rel]", ["l1", "bob", "me"]), - ("link[rel]", ["l1"]), - ("a[rel]", ["bob", "me"]), - ("[lang]", ["lang-en", "lang-en-gb", "lang-en-us", "lang-fr"]), - ("p[class]", ["p1", "pmulti"]), - ("[blah]", []), - ("p[blah]", []), - ("div[data-tag]", ["data1"]), - ) - - def test_quoted_space_in_selector_name(self): - html = """
nope
-
yes
- """ - soup = BeautifulSoup(html, "html.parser") - [chosen] = soup.select('div[style="display: right"]') - assert "yes" == chosen.string - - def test_unsupported_pseudoclass(self): - with pytest.raises(SOUPSIEVE_EXCEPTION_ON_UNSUPPORTED_PSEUDOCLASS): - self._soup.select("a:no-such-pseudoclass") - - with pytest.raises(SelectorSyntaxError): - self._soup.select("a:nth-of-type(a)") - - def test_nth_of_type(self): - # Try to select first paragraph - els = self._soup.select("div#inner p:nth-of-type(1)") - assert len(els) == 1 - assert els[0].string == "Some text" - - # Try to select third paragraph - els = self._soup.select("div#inner p:nth-of-type(3)") - assert len(els) == 1 - assert els[0].string == "Another" - - # Try to select (non-existent!) fourth paragraph - els = self._soup.select("div#inner p:nth-of-type(4)") - assert len(els) == 0 - - # Zero will select no tags. - els = self._soup.select("div p:nth-of-type(0)") - assert len(els) == 0 - - def test_nth_of_type_direct_descendant(self): - els = self._soup.select("div#inner > p:nth-of-type(1)") - assert len(els) == 1 - assert els[0].string == "Some text" - - def test_id_child_selector_nth_of_type(self): - self.assert_css_selects("#inner > p:nth-of-type(2)", ["p1"]) - - def test_select_on_element(self): - # Other tests operate on the tree; this operates on an element - # within the tree. - inner = self._soup.find("div", id="main") - selected = inner.select("div") - # The
tag was selected. The