├── .python-version ├── farasa ├── __init__.py ├── ner.py ├── diacratizer.py ├── stemmer.py ├── lemmatizer.py ├── spellchecker.py ├── segmenter.py ├── pos.py └── __base.py ├── .gitignore ├── pyproject.toml ├── LICENSE ├── README.md ├── tests.py └── uv.lock /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /farasa/__init__.py: -------------------------------------------------------------------------------- 1 | from .diacratizer import FarasaDiacritizer 2 | from .ner import FarasaNamedEntityRecognizer 3 | from .pos import FarasaPOSTagger 4 | from .segmenter import FarasaSegmenter 5 | from .spellchecker import FarasaSpellChecker 6 | from .stemmer import FarasaStemmer 7 | from .lemmatizer import FarasaLemmatizer 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | *.py[cod] 4 | farasa/__pycache__/ 5 | venv/ 6 | 7 | # Installer logs 8 | pip-log.txt 9 | pip-delete-this-directory.txt 10 | 11 | .cache 12 | *.log 13 | *.pot 14 | farasa_bin_obselete/ 15 | lib/ 16 | build/ 17 | dist/ 18 | *.egg-info/ 19 | farasa/farasa_bin/ 20 | farasa/__obselete.py 21 | downloaded_jars/* 22 | -------------------------------------------------------------------------------- /farasa/ner.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaNamedEntityRecognizer(FarasaBase): 5 | task = "NER" 6 | 7 | @property 8 | def command(self): 9 | if self.bin_path is not None: 10 | return self.BASE_CMD + [str(self.bin_path)] 11 | return self.BASE_CMD + [str(self.bin_dir / "FarasaNERJar.jar")] 12 | 13 | def recognize(self, text): 14 | return self.do_task(text=text) 15 | -------------------------------------------------------------------------------- /farasa/diacratizer.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaDiacritizer(FarasaBase): 5 | task = "diacritize" 6 | 7 | @property 8 | def command(self): 9 | if self.bin_path is not None: 10 | return self.BASE_CMD + [str(self.bin_path)] 11 | return self.BASE_CMD + [str(self.bin_dir / "FarasaDiacritizeJar.jar")] 12 | 13 | def diacritize(self, text): 14 | return self.do_task(text=text) 15 | -------------------------------------------------------------------------------- /farasa/stemmer.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaStemmer(FarasaBase): 5 | task = "stem" 6 | 7 | @property 8 | def command(self): 9 | if self.bin_path is not None: 10 | return self.BASE_CMD + [str(self.bin_path)] 11 | return self.BASE_CMD + [ 12 | str(self.bin_dir / "lib" / "FarasaSegmenterJar.jar"), 13 | "-l", 14 | "true", 15 | ] 16 | 17 | def stem(self, text): 18 | return self.do_task(text=text) 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "farasapy" 3 | version = "0.1.1" 4 | description = "A Python Wrapper for the well Farasa toolkit" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "requests~=2.32", 9 | "tqdm~=4.66", 10 | ] 11 | 12 | [project.optional-dependencies] 13 | dev = [ 14 | "black==24.3.0", 15 | "bleach==3.3.0", 16 | "pylint==2.5.2", 17 | ] 18 | 19 | [build-system] 20 | requires = ["hatchling"] 21 | build-backend = "hatchling.build" 22 | 23 | [tool.hatch.build.targets.wheel] 24 | packages = ["farasa"] 25 | -------------------------------------------------------------------------------- /farasa/lemmatizer.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaLemmatizer(FarasaBase): 5 | task = "lemmatize" 6 | is_downloadable = False 7 | 8 | def __init__(self, interactive=False, logging_level="WARNING", binary_path=None): 9 | super().__init__(interactive, logging_level, binary_path) 10 | 11 | @property 12 | def command(self): 13 | if self.bin_path is not None: 14 | return self.BASE_CMD + [str(self.bin_path)] 15 | raise Exception("Binary path for lemmatizer is not provided.") 16 | 17 | def lemmatize(self, text): 18 | return self.do_task(text=text) -------------------------------------------------------------------------------- /farasa/spellchecker.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaSpellChecker(FarasaBase): 5 | task = "spell_check" 6 | 7 | @property 8 | def command(self): 9 | assert ( 10 | not self.interactive 11 | ), "ERROR: Interactive mode is not supported for FarasaSpellCheck. Kindly use standalone mode." 12 | assert ( 13 | self.bin_path is not None 14 | ), "The FarasaSpellCheck jar file is not downloadable by farasapy. Please download it from qcri first then add its path as 'binary_path=' when creating the Spellchecker object." 15 | if self.bin_path is not None: 16 | return self.BASE_CMD + [str(self.bin_path)] 17 | return self.BASE_CMD + [str(self.bin_dir / "FarasaSpellCheck.jar")] 18 | 19 | def spell_check(self, text): 20 | return self.do_task(text=text) 21 | -------------------------------------------------------------------------------- /farasa/segmenter.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class FarasaSegmenter(FarasaBase): 5 | task = "segment" 6 | 7 | @property 8 | def command(self): 9 | if self.bin_path is not None: 10 | return self.BASE_CMD + [str(self.bin_path)] 11 | return self.BASE_CMD + [str(self.bin_dir / "lib" / "FarasaSegmenterJar.jar")] 12 | 13 | def segment(self, text): 14 | return self.do_task(text=text) 15 | 16 | def _desegment_word(self, word: str) -> str: 17 | desegmented_word = word.replace("ل+ال+", "لل") 18 | if "ال+ال" not in word: 19 | desegmented_word = desegmented_word.replace("ل+ال", "لل") 20 | desegmented_word = desegmented_word.replace("+", "") 21 | desegmented_word = desegmented_word.replace("للل", "لل") 22 | return desegmented_word 23 | 24 | def desegment(self, text, separator=" "): 25 | return " ".join(self._desegment_word(word) for word in text.split(separator)) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MagedSaeed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /farasa/pos.py: -------------------------------------------------------------------------------- 1 | from .__base import FarasaBase 2 | 3 | 4 | class TaggedToken: 5 | def __init__(self, token, tag): 6 | self.tokens, self.tags = list(), list() 7 | self.tokens.append(token) 8 | self.tags.append(tag) 9 | 10 | def append_connected_token(self, token=None, tag=None, tagged_token=None): 11 | assert not (token and tag) or not tagged_token 12 | if tagged_token: 13 | self.tokens.extend(tagged_token.tokens) 14 | self.tags.extend(tagged_token.tags) 15 | 16 | else: 17 | self.tokens.append(token) 18 | self.tags.append(tag) 19 | 20 | @property 21 | def last_token(self): 22 | return self.tokens[-1] 23 | 24 | # def raise_value_error(self): 25 | # raise ValueError("Tagged Token object cannot have more than three subtokens") 26 | 27 | def _process_tag(self, tag): 28 | subtags = tag.split("-") 29 | if len(subtags) == 1: 30 | return subtags 31 | else: 32 | return (subtags[0], *list(subtags[1])) 33 | 34 | def process_tags(self): 35 | processed_tags = list() 36 | for tag in self.tags: 37 | processed_subtags = list() 38 | subtags = tag.split("-") 39 | processed_subtags.append(subtags[0]) 40 | if len(subtags) > 1: 41 | for subtag in subtags[1:]: 42 | processed_subtags.append(tuple(subtag)) 43 | processed_tags.append(processed_subtags) 44 | return tuple(processed_tags) 45 | 46 | def as_tuple(self): 47 | return ( 48 | "".join(token.strip() for token in self.tokens).replace("+", ""), 49 | # *list(self._process_tag(tag) for tag in self.tags), 50 | self.process_tags(), 51 | ) 52 | 53 | def __str__(self): 54 | return str(self.as_tuple()) 55 | 56 | def __repr__(self): 57 | return str(self.as_tuple()) 58 | 59 | 60 | class FarasaPOSTagger(FarasaBase): 61 | 62 | @property 63 | def command(self): 64 | if self.bin_path is not None: 65 | return self.BASE_CMD + [str(self.bin_path)] 66 | return self.BASE_CMD + [str(self.bin_dir / "FarasaPOSJar.jar")] 67 | 68 | task = "POS" 69 | 70 | def tag(self, text): 71 | return self.do_task(text=text) 72 | 73 | def tag_segments(self, text, combine_subtokens=False): 74 | tokens_objects = list() 75 | tagged_text = self.tag(text) 76 | # ignore the first and last as they are indications of 77 | # the begining and end of the string 78 | tagged_tokens = tagged_text.split()[1:-1] 79 | subtokens = list() 80 | for tagged_token in tagged_tokens: 81 | slash_count = self._count_slashes(tagged_token) 82 | # if there is no slash, then this must have been a subtoken 83 | if slash_count == 0: 84 | subtokens.append(tagged_token) 85 | # check if the token has multiple slashes ex: date 19/5/1955 or a single slash 86 | elif slash_count > 1: 87 | token = "/".join(tagged_token.split("/")[0:-1]) 88 | tag = tagged_token.split("/")[-1] 89 | tokens_objects.append(TaggedToken(token, tag)) 90 | else: 91 | temp_token, temp_tag = tagged_token.split("/") 92 | if len(subtokens) > 0: 93 | subtokens.append(temp_token) 94 | temp_tags = temp_tag.split("+") 95 | assert len(subtokens) == len(temp_tags) 96 | if combine_subtokens: 97 | tokens_object = TaggedToken(subtokens[0], temp_tags[0]) 98 | for _token, _tag in zip(subtokens[1:], temp_tags[1:]): 99 | tokens_object.append_connected_token(token=_token, tag=_tag) 100 | tokens_objects.append(tokens_object) 101 | else: 102 | for _token, _tag in zip(subtokens, temp_tags): 103 | tokens_objects.append(TaggedToken(_token, _tag)) 104 | subtokens = list() 105 | else: 106 | tokens_objects.append(TaggedToken(temp_token, temp_tag)) 107 | return tokens_objects 108 | 109 | def _count_slashes(self, text: str): 110 | # this is the faster counter implementation for very short text 111 | slash_counter = 0 112 | for c in text: 113 | if c == "/": 114 | slash_counter += 1 115 | return slash_counter 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Table of Content 3 | - [Table of Content](#table-of-content) 4 | - [Disclaimer](#disclaimer) 5 | - [Introduction](#introduction) 6 | - [Installation](#installation) 7 | - [How to use](#how-to-use) 8 | - [AN IMPORTANT REMARK](#an-important-remark) 9 | - [An Overview](#an-overview) 10 | - [Standalone Mode](#standalone-mode) 11 | - [Interactive Mode](#interactive-mode) 12 | - [Caching Support](#caching-support) 13 | - [Contribution](#contribution) 14 | - [Want to cite?](#want-to-cite) 15 | - [Useful URLs](#useful-urls) 16 | 17 |

18 | Open In Colab 19 |

20 | 21 | ![Downloads](https://img.shields.io/pypi/dw/farasapy) 22 | ![License](https://img.shields.io/github/license/magedsaeed/farasapy?style=plastic) 23 | ![PythonVersion](https://img.shields.io/pypi/pyversions/farasapy) 24 | ![PyPiVersion](https://img.shields.io/pypi/v/farasapy?style=plastic) 25 | 26 | # Disclaimer 27 | 28 | >This is a Python API wrapper for [farasa](https://farasa.qcri.org/) [[https://farasa.qcri.org/](https://farasa.qcri.org/)] toolkit. Although this work is licsenced under MIT, the original work(the toolkit) is __strictly premitted for research purposes only__. For any commercial uses, please contact the toolkit creators[https://farasa.qcri.org/]. 29 | 30 | 31 | # Introduction 32 | 33 | Farasa is an Arabic NLP toolkit serving the following tasks: 34 | 1. Segmentation. 35 | 2. Stemming. 36 | 3. Named Entity Recognition (NER). 37 | 4. Part Of Speech tagging (POS tagging). 38 | 5. Diacritization. 39 | 6. Lemmatization. 40 | 41 | The toolkit is built and compiled in Java. Developers who want to use it without using this library may call the binaries directly from their code. 42 | 43 | As Python is a general purpose language and so popular for many NLP tasks, an automation to these calls to the toolkit from the code would be convenient. This is where this wrapper fits. 44 | 45 | # Installation 46 | 47 | ```bash 48 | pip install farasapy 49 | ``` 50 | 51 | # How to use 52 | 53 | > An interactive Google colab code of the library can be reached from here [https://colab.research.google.com/drive/1xjzYwmfAszNzfR6Z2lSQi3nKYcjarXAW?usp=sharing]. 54 | 55 | ## AN IMPORTANT REMARK 56 | 57 | 58 | - The library, as it is a wrapper for Java jars, requires that **Java is installed in your system** and is **in your PATH**. It is, also, not recommended to have a version below Java 1.7. 59 | - Some binaries are computationally HEAVY! 60 | - Some tasks may be slow. 61 | 62 | ## An Overview 63 | 64 | Farasapy wraps and maintains all the toolkit's APIs in different classes where each class is in separate file. You need to import your class of interest from its file as follows: 65 | 66 | ```python 67 | from farasa.pos import FarasaPOSTagger 68 | from farasa.ner import FarasaNamedEntityRecognizer 69 | from farasa.diacratizer import FarasaDiacritizer 70 | from farasa.segmenter import FarasaSegmenter 71 | from farasa.stemmer import FarasaStemmer 72 | from farasa.lemmatizer import FarasaLemmatizer 73 | ``` 74 | 75 | Now, If you are using the library for the first time, the library needs to download farasa toolkit binaries first. You do not need to worry about anything. The library, whenever you instantiate an object of any of its classes, will first check for the binaries, download them if they are not existed. This is an example of instantiating an object from `FarasaStemmer` for the first use of the library. 76 | 77 | ```python 78 | stemmer = FarasaStemmer() 79 | # ouptuts: 80 | ''' 81 | perform system check... 82 | check java version... 83 | Your java version is 1.8 which is compatiple with Farasa 84 | check toolkit binaries... 85 | some binaries are not existed.. 86 | downloading zipped binaries... 87 | 100%|███████████████████████████████████████| 200M/200M [02:39<00:00, 1.26MiB/s] 88 | extracting... 89 | toolkit binaries are downloaded and extracted. 90 | Dependencies seem to be satisfied.. 91 | task [STEM] is initialized in STANDALONE mode... 92 | ''' 93 | ``` 94 | let us *stem* the following example: 95 | ```python 96 | sample =\ 97 | ''' 98 | يُشار إلى أن اللغة العربية يتحدثها أكثر من 422 مليون نسمة ويتوزع متحدثوها 99 | في المنطقة المعروفة باسم الوطن العربي بالإضافة إلى العديد من المناطق ال 100 | أخرى المجاورة مثل الأهواز وتركيا وتشاد والسنغال وإريتريا وغيرها.وهي اللغ 101 | ة الرابعة من لغات منظمة الأمم المتحدة الرسمية الست. 102 | ''' 103 | stemmed_text = stemmer.stem(sample) 104 | print(stemmed_text) 105 | 'أشار إلى أن لغة عربي تحدث أكثر من 422 مليون نسمة توزع متحدثوها في منطقة معروف اسم وطن عربي إضافة إلى عديد من منطقة آخر مجاور مثل أهواز تركيا تشاد سنغال أريتريا غير . هي لغة رابع من لغة منظمة أمة متحد رسمي ست .' 106 | ``` 107 | You may notice that the last line of object instantiation states that the object is instantiated in **STANDALONE** mode. Farasapy, like the toolkit binaries themselves, can run in two different modes: **Interactive** and **Standalone**. 108 | 109 | ### Standalone Mode 110 | 111 | In standalone mode, the instantiated object will call the binary each time it performs its task. It will put the input text in a temporary file, execute the binary with this temporary file, and finally extract the output from another temporary file. These temporary files are garbage collected once the task ends. Be careful that some binaries, *like the diacritizer*, might take very long time to start. Hence, this option is preferred when you have long text and you want to do it only once. 112 | 113 | ### Interactive Mode 114 | 115 | In interactive mode, the object will run the binary once instantiated. It, then, will feed the text to the binary interactively and capture the output on each input. However, the user should be careful not to put large lines as the output, just like in shells, might not be as expected. It is a good practice to *terminate* by `my_obj.terminate()` these kinds of objects once they are not needed to avoid any unexpected behaviour in your code. 116 | 117 | For best practices, use the **INTERACTIVE** mode where the input text is small and you need to do the task multiple times. However, The **STANDALONE** mode is the best for large input texts where the task is expected to be done only once. 118 | 119 | To work on **interactive mode**, you just need to pass `interactive=True` option to your object constructor. 120 | 121 | The following is an example on the segmentation API that is running *interactively*. 122 | 123 | ```python 124 | segmenter = FarasaSegmenter(interactive=True) 125 | # outputs: 126 | ''' 127 | perform system check... 128 | check java version... 129 | Your java version is 1.8 which is compatiple with Farasa 130 | check toolkit binaries... 131 | Dependencies seem to be satisfied.. 132 | /path/to/the/library/farasa/__base.py:40: UserWarning: Be careful with large lines as they may break on interactive mode. You may switch to Standalone mode for such cases. 133 | warnings.warn("Be careful with large lines as they may break on interactive mode. You may switch to Standalone mode for such cases.") 134 | initializing [SEGMENT] task in INTERACTIVE mode... 135 | task [SEGMENT] is initialized interactively. 136 | ''' 137 | 138 | segmented = segmenter.segment(sample) 139 | print(segmented) 140 | 'يشار إلى أن ال+لغ+ة ال+عربي+ة يتحدث+ها أكثر من 422 مليون نسم+ة و+يتوزع متحدثوها في ال+منطق+ة ال+معروف+ة باسم ال+وطن ال+عربي ب+ال+إضاف+ة إلى ال+عديد من ال+مناطق ال+أخرى ال+مجاور+ة مثل ال+أهواز و+تركيا و+تشاد و+ال+سنغال و+إريتريا و+غير+ها . و+هي ال+لغ+ة ال+رابع+ة من لغ+ات منظم+ة ال+أمم ال+متحد+ة ال+رسمي+ة ال+ست .' 141 | ``` 142 | 143 | ## Caching Support 144 | 145 | Farasapy now includes a caching mechanisim to improve performance for repeated operations. By default, caching is **enabled** and results are stored in a default cache folder in ~/.cache (can be configured based on user convenience) to speed up subsequent identical requests. 146 | 147 | ### Basic Usage 148 | 149 | ```python 150 | # Caching is enabled by default 151 | stemmer = FarasaStemmer() 152 | result1 = stemmer.stem("مرحبا") # First call: processes and caches 153 | result2 = stemmer.stem("مرحبا") # Second call: loads from cache (faster) 154 | ``` 155 | 156 | ### Cache Configuration 157 | 158 | You can control caching behavior when creating objects: 159 | 160 | ```python 161 | # Enable caching (default) 162 | stemmer = FarasaStemmer(cache=True) 163 | 164 | # Disable caching 165 | stemmer = FarasaStemmer(cache=False) 166 | 167 | # Custom cache directory 168 | stemmer = FarasaStemmer(cache=True, cache_dir="/path/to/my/cache") # default is ~/.cache, you will find a farasapy folder there. Each task has its own folder. Cached items are key(text)-value(transformed text) json files. 169 | ``` 170 | 171 | Clear cached results when needed: 172 | 173 | ```python 174 | stemmer = FarasaStemmer() 175 | # Process some text... 176 | stemmer.clear_cache() # Clear all cached results for this task 177 | ``` 178 | 179 | Cache Location: 180 | 181 | - **Linux/macOS**: `~/.cache/farasapy/` (follows XDG Cache Directory specification) 182 | - **Windows**: `%LOCALAPPDATA%/farasapy/` 183 | - **Custom**: Specify your own directory with the `cache_dir` parameter 184 | 185 | Cache files are stored in JSON format and organized by task type (stem, segment, etc.). 186 | 187 | # Contribution 188 | 189 | It is my pleasure to give special thanks to those who spend time and effort contributing to farasapy. 190 | 191 | - The credit of desegmentation code goes for @Wissam Antoun [https://github.com/WissamAntoun/Farasa_Desegmenter] in his repository [https://github.com/WissamAntoun/Farasa_Desegmenter]. 192 | - The credit of adding spellchecker support after downloading the binaries JARs from QCRI website goes to @abdullah-shwaiky. 193 | 194 | # Want to cite? 195 | 196 | Farasa is described in this paper for the segmentation task: 197 | ```bibtex 198 | @inproceedings{abdelali2016farasa, 199 | title={Farasa: A fast and furious segmenter for arabic}, 200 | author={Abdelali, Ahmed and Darwish, Kareem and Durrani, Nadir and Mubarak, Hamdy}, 201 | booktitle={Proceedings of the 2016 conference of the North American chapter of the association for computational linguistics: Demonstrations}, 202 | pages={11--16}, 203 | year={2016} 204 | } 205 | ``` 206 | 207 | You can also find the list of publications to site from here: http://qatsdemo.cloudapp.net/farasa/ for the other tasks. 208 | 209 | If you want to cite this tool specifically as a python bridge to Farasa Java toolkit, you can use the following bibtex: 210 | 211 | ```bibtex 212 | @misc{MagedSaeed:online, 213 | author = {Maged Saeed Al-shaibani}, 214 | title = {MagedSaeed/farasapy: A Python implementation of Farasa toolkit}, 215 | howpublished = {\url{https://github.com/MagedSaeed/farasapy}}, 216 | month = {June}, 217 | year = {2021}, 218 | note = {(Accessed on 09/20/2024)} # put your last access here 219 | } 220 | ``` 221 | 222 | # Useful URLs 223 | 224 | - The official site: http://alt.qcri.org/farasa/ 225 | - farasa from GitHub topics: https://github.com/topics/farasa 226 | - A repository by one of the toolkit authors containing WikiNews corpus: https://github.com/kdarwish/Farasa 227 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import json 3 | import os 4 | import tempfile 5 | import time 6 | from pathlib import Path 7 | 8 | from farasa.pos import FarasaPOSTagger 9 | from farasa.ner import FarasaNamedEntityRecognizer 10 | from farasa.diacratizer import FarasaDiacritizer 11 | from farasa.segmenter import FarasaSegmenter 12 | from farasa.stemmer import FarasaStemmer 13 | from farasa.spellchecker import FarasaSpellChecker 14 | from farasa.lemmatizer import FarasaLemmatizer 15 | 16 | # Test samples 17 | sample = """يُشار إلى أن اللغة العربية يتحدثها أكثر من 422 مليون نسمة ويتوزع متحدثوها في المنطقة المعروفة باسم الوطن العربي بالإضافة إلى العديد من المناطق الأخرى المجاورة مثل الأهواز وتركيا وتشاد والسنغال وإريتريا وغيرها. وهي اللغة الرابعة من لغات منظمة الأمم المتحدة الرسمية الست.""" 18 | 19 | spellchecker_sample = """هذا النص خاطؤ الكتابه""" 20 | 21 | simple_test = "اختبار" 22 | 23 | 24 | def test_cache_functionality(): 25 | """Test caching with default settings""" 26 | print("\n=== Testing Cache Functionality ===") 27 | 28 | # Test with cache enabled (default) 29 | print("1. Testing cache enabled (default)...") 30 | stemmer = FarasaStemmer(cache=True, logging_level="DEBUG") 31 | 32 | # First call - should execute and cache 33 | print(" First call (execute and cache):") 34 | start = time.time() 35 | result1 = stemmer.stem(simple_test) 36 | time1 = time.time() - start 37 | print(f" Result: {result1}, Time: {time1:.3f}s") 38 | 39 | # Second call - should use cache 40 | print(" Second call (use cache):") 41 | start = time.time() 42 | result2 = stemmer.stem(simple_test) 43 | time2 = time.time() - start 44 | print(f" Result: {result2}, Time: {time2:.3f}s") 45 | 46 | assert result1 == result2, "Cache results don't match!" 47 | speedup = time1/time2 if time2 > 0 else float('inf') 48 | print(f" ✓ Cache working! Speedup: {speedup:.1f}x") 49 | 50 | # Test with cache disabled 51 | print("2. Testing cache disabled...") 52 | stemmer_no_cache = FarasaStemmer(cache=False, logging_level="DEBUG") 53 | result3 = stemmer_no_cache.stem(simple_test) 54 | assert result1 == result3, "Results differ between cached and non-cached!" 55 | print(" ✓ Cache disabled works correctly") 56 | 57 | 58 | def test_custom_cache_directory(): 59 | """Test custom cache directory parameter""" 60 | print("\n=== Testing Custom Cache Directory ===") 61 | 62 | with tempfile.TemporaryDirectory() as temp_dir: 63 | custom_cache = Path(temp_dir) / "my_custom_cache" 64 | 65 | # Create stemmer with custom cache directory 66 | stemmer = FarasaStemmer(cache=True, cache_dir=str(custom_cache), logging_level="DEBUG") 67 | 68 | # Test that cache directory is created 69 | result = stemmer.stem(simple_test) 70 | 71 | # Verify cache directory structure 72 | assert custom_cache.exists(), "Custom cache directory not created!" 73 | task_dir = custom_cache / "stem" 74 | assert task_dir.exists(), "Task-specific cache directory not created!" 75 | 76 | # Check that cache files exist 77 | cache_files = list(task_dir.glob("*.json")) 78 | assert len(cache_files) > 0, "No cache files created!" 79 | 80 | print(f" ✓ Custom cache directory created at: {custom_cache}") 81 | print(f" ✓ Cache files: {len(cache_files)} found") 82 | 83 | 84 | def test_json_cache_format(): 85 | """Test that cache files are in correct JSON format""" 86 | print("\n=== Testing JSON Cache Format ===") 87 | 88 | with tempfile.TemporaryDirectory() as temp_dir: 89 | cache_dir = Path(temp_dir) / "json_test_cache" 90 | 91 | stemmer = FarasaStemmer(cache=True, cache_dir=str(cache_dir), logging_level="DEBUG") 92 | result = stemmer.stem(simple_test) 93 | 94 | # Find the cache file 95 | cache_files = list((cache_dir / "stem").glob("*.json")) 96 | assert len(cache_files) > 0, "No cache files found!" 97 | 98 | # Read and verify JSON format 99 | cache_file = cache_files[0] 100 | with open(cache_file, 'r', encoding='utf-8') as f: 101 | cache_data = json.load(f) 102 | 103 | # Verify structure: {source_text: result} 104 | assert isinstance(cache_data, dict), "Cache is not a dictionary!" 105 | assert simple_test in cache_data, "Source text not found as key!" 106 | assert cache_data[simple_test] == result, "Cached result doesn't match!" 107 | 108 | print(f" ✓ JSON cache format correct: {cache_data}") 109 | 110 | 111 | def test_cache_clear(): 112 | """Test cache clearing functionality""" 113 | print("\n=== Testing Cache Clear ===") 114 | 115 | with tempfile.TemporaryDirectory() as temp_dir: 116 | cache_dir = Path(temp_dir) / "clear_test_cache" 117 | 118 | # Create and populate cache 119 | stemmer = FarasaStemmer(cache=True, cache_dir=str(cache_dir), logging_level="DEBUG") 120 | result1 = stemmer.stem(simple_test) 121 | result2 = stemmer.stem("نص آخر") 122 | 123 | # Verify cache exists 124 | task_cache_dir = cache_dir / "stem" 125 | cache_files_before = list(task_cache_dir.glob("*.json")) 126 | assert len(cache_files_before) >= 2, "Cache files not created!" 127 | print(f" Cache files before clear: {len(cache_files_before)}") 128 | 129 | # Clear cache 130 | stemmer.clear_cache() 131 | 132 | # Verify cache is cleared 133 | cache_files_after = list(task_cache_dir.glob("*.json")) 134 | assert len(cache_files_after) == 0, "Cache not properly cleared!" 135 | print(" ✓ Cache cleared successfully") 136 | 137 | # Test that new operations still work after clear 138 | result3 = stemmer.stem(simple_test) 139 | assert result3 == result1, "Results differ after cache clear!" 140 | print(" ✓ Operations work correctly after cache clear") 141 | 142 | 143 | def test_cross_platform_cache_paths(): 144 | """Test that default cache paths work cross-platform""" 145 | print("\n=== Testing Cross-Platform Cache Paths ===") 146 | 147 | # Test default cache path selection 148 | stemmer = FarasaStemmer(cache=True, logging_level="DEBUG") 149 | 150 | # Check that cache directory is set correctly 151 | if os.name == 'nt': # Windows 152 | expected_base = Path(os.getenv('LOCALAPPDATA', tempfile.gettempdir())) 153 | else: # Unix-like 154 | expected_base = Path(os.getenv('XDG_CACHE_HOME', Path.home() / '.cache')) 155 | 156 | expected_cache_dir = expected_base / "farasapy" 157 | 158 | assert stemmer.cache_dir == expected_cache_dir, f"Cache dir mismatch: {stemmer.cache_dir} != {expected_cache_dir}" 159 | print(f" ✓ Default cache path correct: {stemmer.cache_dir}") 160 | 161 | 162 | def run_basic_functionality_tests(): 163 | """Run tests for all basic Farasa functionality""" 164 | print("\n=== Testing Basic Functionality (Non-Interactive) ===") 165 | 166 | print("Testing Segmenter...") 167 | segmenter = FarasaSegmenter() 168 | segmented = segmenter.segment(sample) 169 | assert segmented, "Segmentation failed!" 170 | print(f" ✓ Segmented: {segmented[:50]}...") 171 | 172 | print("Testing Stemmer...") 173 | stemmer = FarasaStemmer() 174 | stemmed = stemmer.stem(sample) 175 | assert stemmed, "Stemming failed!" 176 | print(f" ✓ Stemmed: {stemmed[:50]}...") 177 | 178 | print("Testing POS Tagger...") 179 | pos_tagger = FarasaPOSTagger() 180 | pos_tagged = pos_tagger.tag(sample) 181 | assert pos_tagged, "POS tagging failed!" 182 | print(f" ✓ POS Tagged: {pos_tagged[:50]}...") 183 | 184 | print("Testing NER...") 185 | ner = FarasaNamedEntityRecognizer() 186 | ner_result = ner.recognize(sample) 187 | assert ner_result, "NER failed!" 188 | print(f" ✓ NER: {ner_result[:50]}...") 189 | 190 | print("Testing Diacritizer...") 191 | diacritizer = FarasaDiacritizer() 192 | diacritized = diacritizer.diacritize(sample) 193 | assert diacritized, "Diacritization failed!" 194 | print(f" ✓ Diacritized: {diacritized[:50]}...") 195 | 196 | # print("Testing Lemmatizer...") 197 | # print("Please provide the path to the lemmatizer JAR file to run this test.") 198 | # lemmatizer = FarasaLemmatizer(binary_path="") 199 | # lemmatized = lemmatizer.lemmatize(sample) 200 | # print("sample lemmatized:", lemmatized) 201 | 202 | 203 | def run_interactive_mode_tests(): 204 | """Run tests for interactive mode""" 205 | print("\n=== Testing Interactive Mode ===") 206 | 207 | print("Testing Interactive Segmenter...") 208 | segmenter = FarasaSegmenter(interactive=True) 209 | segmented = segmenter.segment(simple_test) 210 | assert segmented, "Interactive segmentation failed!" 211 | print(f" ✓ Interactive Segmented: {segmented}") 212 | 213 | print("Testing Interactive Stemmer...") 214 | stemmer = FarasaStemmer(interactive=True) 215 | stemmed = stemmer.stem(simple_test) 216 | assert stemmed, "Interactive stemming failed!" 217 | print(f" ✓ Interactive Stemmed: {stemmed}") 218 | 219 | print("Testing Interactive POS Tagger...") 220 | pos_tagger = FarasaPOSTagger(interactive=True) 221 | pos_tagged = pos_tagger.tag(simple_test) 222 | assert pos_tagged, "Interactive POS tagging failed!" 223 | print(f" ✓ Interactive POS Tagged: {pos_tagged}") 224 | 225 | print("Testing Interactive NER...") 226 | ner = FarasaNamedEntityRecognizer(interactive=True) 227 | ner_result = ner.recognize(simple_test) 228 | assert ner_result, "Interactive NER failed!" 229 | print(f" ✓ Interactive NER: {ner_result}") 230 | 231 | print("Testing Interactive Diacritizer...") 232 | diacritizer = FarasaDiacritizer(interactive=True) 233 | diacritized = diacritizer.diacritize(simple_test) 234 | assert diacritized, "Interactive diacritization failed!" 235 | print(f" ✓ Interactive Diacritized: {diacritized}") 236 | 237 | # print("Testing Lemmatizer...") 238 | # print("Please provide the path to the lemmatizer JAR file to run this test.") 239 | # lemmatizer = FarasaLemmatizer(interactive=True,binary_path="") 240 | # lemmatized = lemmatizer.lemmatize(sample) 241 | # print("sample lemmatized:", lemmatized) 242 | 243 | 244 | def main(): 245 | """Run all tests""" 246 | print("=" * 60) 247 | print("FARASAPY TEST SUITE") 248 | print("=" * 60) 249 | 250 | try: 251 | # Test cache functionality 252 | test_cache_functionality() 253 | test_custom_cache_directory() 254 | test_json_cache_format() 255 | test_cache_clear() 256 | test_cross_platform_cache_paths() 257 | 258 | # Test basic functionality 259 | run_basic_functionality_tests() 260 | run_interactive_mode_tests() 261 | 262 | print("\n" + "=" * 60) 263 | print("✅ ALL TESTS PASSED!") 264 | print("=" * 60) 265 | 266 | except Exception as e: 267 | print(f"\n❌ TEST FAILED: {e}") 268 | raise 269 | 270 | 271 | if __name__ == "__main__": 272 | main() -------------------------------------------------------------------------------- /farasa/__base.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import io 3 | import json 4 | import logging 5 | import os 6 | import re 7 | import subprocess 8 | import sys 9 | import tempfile 10 | import warnings 11 | import zipfile 12 | from pathlib import Path 13 | 14 | import requests 15 | from tqdm import tqdm 16 | 17 | 18 | class FarasaBase: 19 | task = None 20 | base_dir = Path(__file__).parent.absolute() 21 | bin_dir = Path(f"{base_dir}/farasa_bin") 22 | # bin_lib_dir = Path(f"{bin_dir}/lib") 23 | bin_path = None 24 | 25 | # shlex not compatible with Windows replace it with list() 26 | # set java encoding with option `-Dfile.encoding=UTF-8` 27 | BASE_CMD = ["java", "-Dfile.encoding=UTF-8", "-jar"] 28 | interactive = False 29 | task_proc = None 30 | logger = None 31 | is_downloadable = True 32 | 33 | def __init__(self, interactive=False, logging_level="WARNING", binary_path=None, cache=True, cache_dir=None): 34 | self.config_logs(logging_level) 35 | self.cache_enabled = cache 36 | 37 | # Set cache directory: user-provided, or OS-appropriate default 38 | if cache_dir is not None: 39 | self.cache_dir = Path(cache_dir) 40 | else: 41 | # Use XDG cache directory on Linux/Unix, LOCALAPPDATA on Windows 42 | if os.name == 'nt': # Windows 43 | cache_base = Path(os.getenv('LOCALAPPDATA', tempfile.gettempdir())) 44 | else: # Unix-like (Linux, macOS, etc.) 45 | cache_base = Path(os.getenv('XDG_CACHE_HOME', Path.home() / '.cache')) 46 | self.cache_dir = cache_base / "farasapy" 47 | if self.cache_enabled: 48 | self._setup_cache() 49 | self.logger.debug("perform system check...") 50 | self.logger.debug("check java version...") 51 | self.check_java_version() 52 | self.logger.debug("check toolkit binaries...") 53 | if binary_path is not None: 54 | self.bin_path = Path(binary_path) 55 | # assert the path exists as a file 56 | assert Path(self.bin_path).is_file(), f"{self.bin_path} not found" 57 | else: 58 | # binaries that should be downloaded from qcri 59 | if self.is_downloadable: 60 | self.check_toolkit_binaries() 61 | else: 62 | raise Exception( 63 | """Binaries of this task are not downloadable automatically! 64 | Please download them from qcri website manually, extract them, then set 'binary_path' to the extracted JAR file path of this task.""" 65 | ) 66 | Path(f"{self.base_dir}/tmp").mkdir(exist_ok=True) 67 | self.logger.info("Dependencies seem to be satisfied..") 68 | if interactive: 69 | self.interactive = True 70 | self.logger.warning( 71 | "Be careful with large lines as they may break on interactive mode. You may switch to Standalone mode for such cases." 72 | ) 73 | self.logger.info( 74 | f"\033[37minitializing [{self.task.upper()}] task in \033[32mINTERACTIVE \033[37mmode..." 75 | ) 76 | self.initialize_task() 77 | self.logger.info( 78 | f"task [{self.task.upper()}] is initialized interactively." 79 | ) 80 | else: 81 | self.logger.info( 82 | f"task [{self.task.upper()}] is initialized in \033[34mSTANDALONE \033[37mmode..." 83 | ) 84 | 85 | @property 86 | def command(self): 87 | """ 88 | This function should return the CMD command to be executed for the task. 89 | """ 90 | raise NotImplemented 91 | 92 | def config_logs(self, logging_level): 93 | self.logger = logging.getLogger("farasapy_logger") 94 | self.logger.propagate = False 95 | self.logger.setLevel(getattr(logging, logging_level.upper())) 96 | logs_formatter = logging.Formatter( 97 | "[%(asctime)s - %(name)s - %(levelname)s]: %(message)s" 98 | ) 99 | if not self.logger.hasHandlers(): 100 | stream_logger = logging.StreamHandler() 101 | stream_logger.setFormatter(logs_formatter) 102 | self.logger.addHandler(stream_logger) 103 | 104 | def check_java_version(self): 105 | try: 106 | version_proc_output = subprocess.check_output( 107 | ["java", "-version"], stderr=subprocess.STDOUT, encoding="utf8" 108 | ) 109 | # version_pattern = r"\"(\d+\.\d+).*\"" 110 | version_pattern = r"\"(\d+(\.\d+){0,1})" 111 | java_version = float( 112 | re.search(version_pattern, version_proc_output).groups()[0] 113 | ) 114 | if java_version >= 1.7: 115 | self.logger.debug( 116 | f"Your java version is {java_version} which is compatible with Farasa " 117 | ) 118 | else: 119 | warnings.warn( 120 | "You are using old version of java. Farasa is compatible with Java 7 and above " 121 | ) 122 | except subprocess.CalledProcessError as proc_err: 123 | self.logger.error(f"error occurred: {proc_err}.") 124 | raise Exception( 125 | "We could not check for java version on the machine. Please make sure you have installed Java 1.7+ and add it to your PATH." 126 | ) 127 | 128 | def check_toolkit_binaries(self): 129 | download = False 130 | # check in bin folder: 131 | for jar in ("FarasaNERJar", "FarasaPOSJar", "FarasaDiacritizeJar"): 132 | if not Path(f"{self.bin_dir}/{jar}.jar").is_file(): 133 | download = True 134 | break 135 | 136 | if ( 137 | download or not Path(f"{self.bin_dir}/lib/FarasaSegmenterJar.jar").is_file() 138 | ): # last check for binaries in farasa_bin/lib 139 | self.logger.info("some binaries does not exist. Downloading...") 140 | self.download_binaries() 141 | 142 | def get_content_with_progressbar(self, request): 143 | totalsize = int(request.headers.get("content-length", 0)) 144 | blocksize = 3413334 145 | bar = tqdm( 146 | total=totalsize, 147 | unit="iB", 148 | unit_scale=True, 149 | ncols=5, 150 | dynamic_ncols=True, 151 | file=sys.stdout, 152 | ) 153 | content = None 154 | for data in request.iter_content(blocksize): 155 | bar.update(len(data)) 156 | if content is None: 157 | content = data 158 | else: 159 | content += data 160 | return content 161 | 162 | def download_binaries(self): 163 | self.logger.info("downloading zipped binaries...") 164 | try: 165 | # change download url from github releases to qcri 166 | # binaries_url = "https://github.com/MagedSaeed/farasapy/releases/download/toolkit-bins-released/farasa_bin.zip" 167 | binaries_url = "https://farasa-api.qcri.org/farasapy/releases/download/toolkit-bins-released/farasa_bin.zip" 168 | binaries_request = requests.get(binaries_url, stream=True, verify=False) 169 | # show the progress bar while getting content 170 | content_bytes = self.get_content_with_progressbar(binaries_request) 171 | self.logger.debug("extracting...") 172 | binzip = zipfile.ZipFile(io.BytesIO(content_bytes)) 173 | binzip.extractall(path=self.base_dir) 174 | self.logger.debug("toolkit binaries are downloaded and extracted.") 175 | except Exception as e: 176 | self.logger.error("an error occurred") 177 | self.logger.error(e) 178 | 179 | def initialize_task_proc(self): 180 | self.task_proc = subprocess.Popen( 181 | self.command, 182 | stdin=subprocess.PIPE, 183 | stdout=subprocess.PIPE, 184 | stderr=subprocess.PIPE, 185 | ) 186 | 187 | def initialize_task(self): 188 | word = "اختبار" 189 | word += "\n" 190 | bword = str.encode(word) 191 | self.initialize_task_proc() 192 | return self.run_task_interactive(bword) 193 | 194 | def run_task_standalone(self, btext): 195 | assert btext is not None 196 | tmpdir = str(self.base_dir / "tmp") 197 | # if delete=True on Windows cannot get any content 198 | # https://docs.python.org/3/library/tempfile.html#tempfile.NamedTemporaryFile 199 | itmp = tempfile.NamedTemporaryFile(dir=tmpdir, delete=False) 200 | otmp = tempfile.NamedTemporaryFile(dir=tmpdir, delete=False) 201 | try: 202 | itmp.write(btext) 203 | # https://stackoverflow.com/questions/46004774/python-namedtemporaryfile-appears-empty-even-after-data-is-written 204 | itmp.flush() 205 | proc = subprocess.run( 206 | self.command + ["-i", itmp.name, "-o", otmp.name], 207 | stdout=subprocess.DEVNULL, 208 | stderr=subprocess.DEVNULL, 209 | # this only compatiple with python>3.6 210 | # capture_output=True, 211 | ) 212 | if proc.returncode == 0: 213 | result = otmp.read().decode("utf8").strip() 214 | else: 215 | self.logger.critical( 216 | f"error occurred! stdout: , {proc.stdout}, stderr: , {proc.stderr}" 217 | ) 218 | self.logger.critical(f"return code: {proc.returncode}") 219 | raise Exception("Internal Error occurred!") 220 | finally: 221 | itmp.close() 222 | otmp.close() 223 | os.unlink(itmp.name) 224 | os.unlink(otmp.name) 225 | return result 226 | 227 | def run_task_interactive(self, btext): 228 | assert isinstance(btext, bytes) 229 | assert self.interactive 230 | try: 231 | self.task_proc.stdin.flush() 232 | self.task_proc.stdin.write(btext) 233 | self.task_proc.stdin.flush() 234 | except BrokenPipeError as broken_pipe: 235 | self.logger.error( 236 | f"pipe broke! error code and message: [{broken_pipe}]. reinitialize the process.., This may take sometime depending on the running task" 237 | ) 238 | self.initialize_task_proc() 239 | self.task_proc.stdin.flush() 240 | self.task_proc.stdin.write(btext) 241 | self.task_proc.stdin.flush() 242 | 243 | output = self.task_proc.stdout.readline().decode("utf8").strip() 244 | self.task_proc.stdout.flush() 245 | return output 246 | 247 | def do_task_interactive(self, strip_text): 248 | outputs = [] 249 | for line in strip_text.split("\n"): 250 | newlined_line = line + "\n" 251 | byted_newlined_line = str.encode(newlined_line) 252 | output = self.run_task_interactive(byted_newlined_line) 253 | if output: 254 | outputs.append(output) 255 | return "\n".join(outputs) 256 | 257 | def do_task_standalone(self, strip_text): 258 | byted_strip_text = str.encode(strip_text) 259 | return self.run_task_standalone(btext=byted_strip_text) 260 | 261 | def do_task(self, text): 262 | strip_text = text.strip() 263 | 264 | # Store current text for cache saving 265 | self._current_text = strip_text 266 | 267 | # Try to load from cache first 268 | cache_key = self._get_cache_key(strip_text) 269 | cached_result = self._load_from_cache(cache_key) 270 | if cached_result is not None: 271 | return cached_result 272 | 273 | # Execute the task if not in cache 274 | if self.interactive: 275 | result = self.do_task_interactive(strip_text) 276 | else: 277 | result = self.do_task_standalone(strip_text) 278 | 279 | # Save result to cache 280 | self._save_to_cache(cache_key, result) 281 | return result 282 | 283 | def terminate(self): 284 | self.task_proc.terminate() 285 | 286 | def clear_cache(self): 287 | """Clear all cached results for this task""" 288 | if not self.cache_enabled: 289 | self.logger.info("Cache is disabled, nothing to clear.") 290 | return 291 | 292 | task_cache_dir = self.cache_dir / self.task 293 | if task_cache_dir.exists(): 294 | try: 295 | import shutil 296 | shutil.rmtree(task_cache_dir) 297 | task_cache_dir.mkdir(exist_ok=True) 298 | self.logger.info(f"Cache cleared for task: {self.task}") 299 | except Exception as e: 300 | self.logger.warning(f"Failed to clear cache: {e}") 301 | else: 302 | self.logger.info(f"No cache directory found for task: {self.task}") 303 | 304 | def _setup_cache(self): 305 | """Create cache directory structure""" 306 | try: 307 | self.cache_dir.mkdir(exist_ok=True) 308 | task_cache_dir = self.cache_dir / self.task 309 | task_cache_dir.mkdir(exist_ok=True) 310 | self.logger.debug(f"Cache directory set up at {self.cache_dir}") 311 | except Exception as e: 312 | self.logger.warning(f"Failed to setup cache directory: {e}. Disabling cache.") 313 | self.cache_enabled = False 314 | 315 | def _get_cache_key(self, text): 316 | """Generate a unique cache key for the given text and current configuration""" 317 | # Include task type, interactive mode, and text content in the hash 318 | cache_data = f"{self.task}:{'interactive' if self.interactive else 'standalone'}:{text}" 319 | return hashlib.sha256(cache_data.encode('utf-8')).hexdigest() 320 | 321 | def _get_cache_path(self, cache_key): 322 | """Get the full path to the cache file""" 323 | return self.cache_dir / self.task / f"{cache_key}.json" 324 | 325 | def _load_from_cache(self, cache_key): 326 | """Load result from cache if it exists""" 327 | if not self.cache_enabled: 328 | return None 329 | 330 | cache_path = self._get_cache_path(cache_key) 331 | if cache_path.exists(): 332 | try: 333 | with open(cache_path, 'r', encoding='utf-8') as f: 334 | cache_data = json.load(f) 335 | # Get the first (and only) value from the key-value pair 336 | result = next(iter(cache_data.values())) 337 | self.logger.debug(f"Cache hit for key: {cache_key[:8]}...") 338 | return result 339 | except Exception as e: 340 | self.logger.warning(f"Failed to load from cache: {e}") 341 | return None 342 | 343 | def _save_to_cache(self, cache_key, result): 344 | """Save result to cache""" 345 | if not self.cache_enabled: 346 | return 347 | 348 | cache_path = self._get_cache_path(cache_key) 349 | try: 350 | # Get the original text from the current task 351 | original_text = getattr(self, '_current_text', '') 352 | cache_data = {original_text: result} 353 | 354 | with open(cache_path, 'w', encoding='utf-8') as f: 355 | json.dump(cache_data, f, ensure_ascii=False, indent=2) 356 | self.logger.debug(f"Cached result for key: {cache_key[:8]}...") 357 | except Exception as e: 358 | self.logger.warning(f"Failed to save to cache: {e}") 359 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 3 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "astroid" 7 | version = "2.5" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "lazy-object-proxy" }, 11 | { name = "wrapt" }, 12 | ] 13 | sdist = { url = "https://files.pythonhosted.org/packages/18/00/753b485627d9cd642516c195db63dcc0c87c36b8c682125c000b7f611b09/astroid-2.5.tar.gz", hash = "sha256:b31c92f545517dcc452f284bc9c044050862fbe6d93d2b3de4a215a6b384bf0d", size = 304788, upload-time = "2021-02-15T20:45:08.578Z" } 14 | wheels = [ 15 | { url = "https://files.pythonhosted.org/packages/ff/f0/2364d469327ffef8ee1964a5995f8206fd22fcfa57f2618498f8b963329f/astroid-2.5-py3-none-any.whl", hash = "sha256:87ae7f2398b8a0ae5638ddecf9987f081b756e0e9fc071aeebdca525671fc4dc", size = 220285, upload-time = "2021-02-15T20:45:04.222Z" }, 16 | ] 17 | 18 | [[package]] 19 | name = "black" 20 | version = "24.3.0" 21 | source = { registry = "https://pypi.org/simple" } 22 | dependencies = [ 23 | { name = "click" }, 24 | { name = "mypy-extensions" }, 25 | { name = "packaging" }, 26 | { name = "pathspec" }, 27 | { name = "platformdirs" }, 28 | { name = "tomli", marker = "python_full_version < '3.11'" }, 29 | { name = "typing-extensions", marker = "python_full_version < '3.11'" }, 30 | ] 31 | sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292, upload-time = "2024-03-15T19:35:43.699Z" } 32 | wheels = [ 33 | { url = "https://files.pythonhosted.org/packages/3b/32/1a25d1b83147ca128797a627f429f9dc390eb066805c6aa319bea3ffffa5/black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", size = 1587891, upload-time = "2024-03-15T19:43:32.908Z" }, 34 | { url = "https://files.pythonhosted.org/packages/c4/91/6cb204786acc693edc4bf1b9230ffdc3cbfaeb7cd04d3a12fb4b13882a53/black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", size = 1434886, upload-time = "2024-03-15T19:41:59.067Z" }, 35 | { url = "https://files.pythonhosted.org/packages/ef/e4/53b5d07117381f7d5e946a54dd4c62617faad90713649619bbc683769dfe/black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7", size = 1747400, upload-time = "2024-03-15T19:38:22.142Z" }, 36 | { url = "https://files.pythonhosted.org/packages/13/9c/f2e7532d11b05add5ab383a9f90be1a49954bf510803f98064b45b42f98e/black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", size = 1363816, upload-time = "2024-03-15T19:39:43.559Z" }, 37 | { url = "https://files.pythonhosted.org/packages/68/df/ceea5828be9c4931cb5a75b7e8fb02971f57524da7a16dfec0d4d575327f/black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", size = 1571235, upload-time = "2024-03-15T19:45:27.77Z" }, 38 | { url = "https://files.pythonhosted.org/packages/46/5f/30398c5056cb72f883b32b6520ad00042a9d0454b693f70509867db03a80/black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", size = 1414926, upload-time = "2024-03-15T19:43:52.993Z" }, 39 | { url = "https://files.pythonhosted.org/packages/6b/59/498885b279e890f656ea4300a2671c964acb6d97994ea626479c2e5501b4/black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", size = 1725920, upload-time = "2024-03-15T19:38:13.052Z" }, 40 | { url = "https://files.pythonhosted.org/packages/8f/b0/4bef40c808cc615187db983b75bacdca1c110a229d41ba9887549fac529c/black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", size = 1372608, upload-time = "2024-03-15T19:39:34.973Z" }, 41 | { url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822, upload-time = "2024-03-15T19:45:20.337Z" }, 42 | { url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987, upload-time = "2024-03-15T19:45:00.637Z" }, 43 | { url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319, upload-time = "2024-03-15T19:38:24.009Z" }, 44 | { url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180, upload-time = "2024-03-15T19:39:37.014Z" }, 45 | { url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493, upload-time = "2024-03-15T19:35:41.572Z" }, 46 | ] 47 | 48 | [[package]] 49 | name = "bleach" 50 | version = "3.3.0" 51 | source = { registry = "https://pypi.org/simple" } 52 | dependencies = [ 53 | { name = "packaging" }, 54 | { name = "six" }, 55 | { name = "webencodings" }, 56 | ] 57 | sdist = { url = "https://files.pythonhosted.org/packages/70/84/2783f734240fab7815a00b419c4281d2d0984971de30b08176aae2acff10/bleach-3.3.0.tar.gz", hash = "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433", size = 181274, upload-time = "2021-02-01T17:15:30.138Z" } 58 | wheels = [ 59 | { url = "https://files.pythonhosted.org/packages/f0/46/2bbd92086a4c6f051214cb48df6d9132b5f32c5e881d3f4991b16ec7e499/bleach-3.3.0-py2.py3-none-any.whl", hash = "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", size = 283459, upload-time = "2021-02-01T17:15:27.936Z" }, 60 | ] 61 | 62 | [[package]] 63 | name = "certifi" 64 | version = "2025.8.3" 65 | source = { registry = "https://pypi.org/simple" } 66 | sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } 67 | wheels = [ 68 | { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, 69 | ] 70 | 71 | [[package]] 72 | name = "charset-normalizer" 73 | version = "3.4.3" 74 | source = { registry = "https://pypi.org/simple" } 75 | sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } 76 | wheels = [ 77 | { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, 78 | { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, 79 | { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, 80 | { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, 81 | { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, 82 | { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, 83 | { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, 84 | { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, 85 | { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, 86 | { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, 87 | { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, 88 | { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, 89 | { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, 90 | { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, 91 | { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, 92 | { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, 93 | { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, 94 | { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, 95 | { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, 96 | { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, 97 | { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, 98 | { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, 99 | { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, 100 | { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, 101 | { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, 102 | { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, 103 | { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, 104 | { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, 105 | { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, 106 | { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, 107 | { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, 108 | { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, 109 | { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, 110 | { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, 111 | { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, 112 | { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, 113 | { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, 114 | { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, 115 | { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, 116 | { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, 117 | { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, 118 | { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, 119 | { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, 120 | { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, 121 | { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, 122 | { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, 123 | { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, 124 | { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, 125 | { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, 126 | { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, 127 | { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, 128 | { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, 129 | { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, 130 | { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, 131 | { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, 132 | { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, 133 | ] 134 | 135 | [[package]] 136 | name = "click" 137 | version = "8.2.1" 138 | source = { registry = "https://pypi.org/simple" } 139 | dependencies = [ 140 | { name = "colorama", marker = "sys_platform == 'win32'" }, 141 | ] 142 | sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } 143 | wheels = [ 144 | { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, 145 | ] 146 | 147 | [[package]] 148 | name = "colorama" 149 | version = "0.4.6" 150 | source = { registry = "https://pypi.org/simple" } 151 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 152 | wheels = [ 153 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 154 | ] 155 | 156 | [[package]] 157 | name = "farasapy" 158 | version = "0.1.0" 159 | source = { virtual = "." } 160 | dependencies = [ 161 | { name = "black" }, 162 | { name = "bleach" }, 163 | { name = "pylint" }, 164 | { name = "requests" }, 165 | { name = "tqdm" }, 166 | ] 167 | 168 | [package.metadata] 169 | requires-dist = [ 170 | { name = "black", specifier = "==24.3.0" }, 171 | { name = "bleach", specifier = "==3.3.0" }, 172 | { name = "pylint", specifier = "==2.5.2" }, 173 | { name = "requests", specifier = "==2.32.3" }, 174 | { name = "tqdm", specifier = "==4.66.3" }, 175 | ] 176 | 177 | [[package]] 178 | name = "idna" 179 | version = "3.10" 180 | source = { registry = "https://pypi.org/simple" } 181 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 182 | wheels = [ 183 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 184 | ] 185 | 186 | [[package]] 187 | name = "isort" 188 | version = "4.3.21" 189 | source = { registry = "https://pypi.org/simple" } 190 | sdist = { url = "https://files.pythonhosted.org/packages/43/00/8705e8d0c05ba22f042634f791a61f4c678c32175763dcf2ca2a133f4739/isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", size = 69546, upload-time = "2019-06-26T01:12:56.466Z" } 191 | wheels = [ 192 | { url = "https://files.pythonhosted.org/packages/e5/b0/c121fd1fa3419ea9bfd55c7f9c4fedfec5143208d8c7ad3ce3db6c623c21/isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd", size = 42281, upload-time = "2019-06-26T01:12:54.587Z" }, 193 | ] 194 | 195 | [[package]] 196 | name = "lazy-object-proxy" 197 | version = "1.12.0" 198 | source = { registry = "https://pypi.org/simple" } 199 | sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } 200 | wheels = [ 201 | { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, 202 | { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, 203 | { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, 204 | { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, 205 | { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, 206 | { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, 207 | { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, 208 | { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, 209 | { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, 210 | { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, 211 | { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, 212 | { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, 213 | { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, 214 | { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, 215 | { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, 216 | { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, 217 | { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, 218 | { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, 219 | { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, 220 | { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, 221 | { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, 222 | { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, 223 | { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, 224 | { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, 225 | { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, 226 | { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, 227 | { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, 228 | { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, 229 | { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, 230 | { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, 231 | { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, 232 | { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, 233 | { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, 234 | { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, 235 | { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, 236 | { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, 237 | { url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" }, 238 | ] 239 | 240 | [[package]] 241 | name = "mccabe" 242 | version = "0.6.1" 243 | source = { registry = "https://pypi.org/simple" } 244 | sdist = { url = "https://files.pythonhosted.org/packages/06/18/fa675aa501e11d6d6ca0ae73a101b2f3571a565e0f7d38e062eec18a91ee/mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f", size = 8612, upload-time = "2017-01-26T22:13:15.699Z" } 245 | wheels = [ 246 | { url = "https://files.pythonhosted.org/packages/87/89/479dc97e18549e21354893e4ee4ef36db1d237534982482c3681ee6e7b57/mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", size = 8556, upload-time = "2017-01-26T22:13:14.36Z" }, 247 | ] 248 | 249 | [[package]] 250 | name = "mypy-extensions" 251 | version = "1.1.0" 252 | source = { registry = "https://pypi.org/simple" } 253 | sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } 254 | wheels = [ 255 | { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, 256 | ] 257 | 258 | [[package]] 259 | name = "packaging" 260 | version = "25.0" 261 | source = { registry = "https://pypi.org/simple" } 262 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 263 | wheels = [ 264 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 265 | ] 266 | 267 | [[package]] 268 | name = "pathspec" 269 | version = "0.12.1" 270 | source = { registry = "https://pypi.org/simple" } 271 | sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } 272 | wheels = [ 273 | { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, 274 | ] 275 | 276 | [[package]] 277 | name = "platformdirs" 278 | version = "4.4.0" 279 | source = { registry = "https://pypi.org/simple" } 280 | sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } 281 | wheels = [ 282 | { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, 283 | ] 284 | 285 | [[package]] 286 | name = "pylint" 287 | version = "2.5.2" 288 | source = { registry = "https://pypi.org/simple" } 289 | dependencies = [ 290 | { name = "astroid" }, 291 | { name = "colorama", marker = "sys_platform == 'win32'" }, 292 | { name = "isort" }, 293 | { name = "mccabe" }, 294 | { name = "toml" }, 295 | ] 296 | sdist = { url = "https://files.pythonhosted.org/packages/b4/2d/c209a539c32e2d9c7396e69bab95ad4332b73cdc360bbc89495d7e49d332/pylint-2.5.2.tar.gz", hash = "sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c", size = 683111, upload-time = "2020-05-05T13:00:22.786Z" } 297 | wheels = [ 298 | { url = "https://files.pythonhosted.org/packages/37/6e/36419ec1bd2208e157dff7fc3e565b185394c0dc4901e9e2f983cb1d4b7f/pylint-2.5.2-py3-none-any.whl", hash = "sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b", size = 324316, upload-time = "2020-05-05T13:00:21.083Z" }, 299 | ] 300 | 301 | [[package]] 302 | name = "requests" 303 | version = "2.32.3" 304 | source = { registry = "https://pypi.org/simple" } 305 | dependencies = [ 306 | { name = "certifi" }, 307 | { name = "charset-normalizer" }, 308 | { name = "idna" }, 309 | { name = "urllib3" }, 310 | ] 311 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } 312 | wheels = [ 313 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, 314 | ] 315 | 316 | [[package]] 317 | name = "six" 318 | version = "1.17.0" 319 | source = { registry = "https://pypi.org/simple" } 320 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, 323 | ] 324 | 325 | [[package]] 326 | name = "toml" 327 | version = "0.10.2" 328 | source = { registry = "https://pypi.org/simple" } 329 | sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } 330 | wheels = [ 331 | { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, 332 | ] 333 | 334 | [[package]] 335 | name = "tomli" 336 | version = "2.2.1" 337 | source = { registry = "https://pypi.org/simple" } 338 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 339 | wheels = [ 340 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 341 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 342 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 343 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 344 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 345 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 346 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 347 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 348 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 349 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 350 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 351 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 352 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 353 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 354 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 355 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 356 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 357 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 358 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 359 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 360 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 361 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 362 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 363 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 364 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 365 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 366 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 367 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 368 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 369 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 370 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 371 | ] 372 | 373 | [[package]] 374 | name = "tqdm" 375 | version = "4.66.3" 376 | source = { registry = "https://pypi.org/simple" } 377 | dependencies = [ 378 | { name = "colorama", marker = "sys_platform == 'win32'" }, 379 | ] 380 | sdist = { url = "https://files.pythonhosted.org/packages/03/00/6a9b3aedb0b60a80995ade30f718f1a9902612f22a1aaf531c85a02987f7/tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5", size = 169551, upload-time = "2024-05-02T21:44:05.084Z" } 381 | wheels = [ 382 | { url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload-time = "2024-05-02T21:44:01.541Z" }, 383 | ] 384 | 385 | [[package]] 386 | name = "typing-extensions" 387 | version = "4.15.0" 388 | source = { registry = "https://pypi.org/simple" } 389 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } 390 | wheels = [ 391 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, 392 | ] 393 | 394 | [[package]] 395 | name = "urllib3" 396 | version = "2.5.0" 397 | source = { registry = "https://pypi.org/simple" } 398 | sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } 399 | wheels = [ 400 | { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, 401 | ] 402 | 403 | [[package]] 404 | name = "webencodings" 405 | version = "0.5.1" 406 | source = { registry = "https://pypi.org/simple" } 407 | sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } 408 | wheels = [ 409 | { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, 410 | ] 411 | 412 | [[package]] 413 | name = "wrapt" 414 | version = "1.12.1" 415 | source = { registry = "https://pypi.org/simple" } 416 | sdist = { url = "https://files.pythonhosted.org/packages/82/f7/e43cefbe88c5fd371f4cf0cf5eb3feccd07515af9fd6cf7dbf1d1793a797/wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7", size = 27488, upload-time = "2020-03-09T02:32:04.07Z" } 417 | --------------------------------------------------------------------------------