├── setup.cfg ├── tests ├── data │ └── voice │ │ └── hello_world_us.mp3 ├── __init__.py ├── test_voice.py └── test_context.py ├── reverso_api ├── __init__.py ├── voice.py └── context.py ├── examples ├── voice │ ├── hello_world.py │ └── little_kitten_cn_en.py └── context │ └── mini_reverso_context.py ├── LICENSE ├── setup.py ├── README.md └── .gitignore /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /tests/data/voice/hello_world_us.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demian-wolf/pyreverso/HEAD/tests/data/voice/hello_world_us.mp3 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from .test_voice import TestReversoVoiceAPI 4 | from .test_context import TestReversoContextAPI 5 | 6 | 7 | if __name__ == "__main__": 8 | unittest.main() 9 | -------------------------------------------------------------------------------- /reverso_api/__init__.py: -------------------------------------------------------------------------------- 1 | """Reverso.net API for Python""" 2 | 3 | from .context import * 4 | from .voice import * 5 | 6 | 7 | __author__ = "Demian Volkov" 8 | __copyright__ = "Copyright 2020-2021, Demian Volkov" 9 | __credits__ = ["Demian Volkov"] 10 | __license__ = "MIT" 11 | __version__ = "0.0.1.beta.3" 12 | __maintainer__ = "Demian Wolf" 13 | __email__ = "demianwolfssd@gmail.com" 14 | -------------------------------------------------------------------------------- /examples/voice/hello_world.py: -------------------------------------------------------------------------------- 1 | """'Hello, World!' sentence said in all available 'US English' voices. 2 | 3 | Note: pygame is required for this example to work.""" 4 | 5 | 6 | from reverso_api import ReversoVoiceAPI 7 | 8 | 9 | api = ReversoVoiceAPI(text="Hello, World!", voice="Heather22k") 10 | 11 | for voice in api.voices["US English"]: 12 | api.say("Hello, World!", voice, wait=True) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Demian Wolf 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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="Reverso_API", 9 | version="0.0.1.beta.3", 10 | license="MIT", 11 | author="Demian Volkov", 12 | author_email="demianwolfssd@gmail.com", 13 | description="Reverso.net API for Python", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/demian-wolf/ReversoAPI", 17 | download_url="https://github.com/demian-wolf/ReversoAPI/archive/v0.0.1.beta.3.tar.gz", 18 | keywords=["REVERSO", "REVERSO-CONTEXT", "REVERSO CONTEXT", "CONTEXT", "REVERSO-VOICE", "REVERSO VOICE", "VOICE", 19 | "REVERSO-API", "API", "WRAPPER", "PYTHON"], 20 | packages=setuptools.find_packages(), 21 | install_requires=[ 22 | "requests", 23 | "beautifulsoup4", 24 | "lxml", 25 | ], 26 | extras_require={ 27 | "playing spoken text instead of just getting its MP3 data and/or saving it to file-like objects": ["pygame"], 28 | }, 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Development Status :: 4 - Beta", 34 | "Intended Audience :: Developers", 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /examples/voice/little_kitten_cn_en.py: -------------------------------------------------------------------------------- 1 | """'Can I adopt your little kitten?' sentence spoken in Chinese and in English with random chosen voices. 2 | 3 | Note: pygame is required for this example to work.""" 4 | 5 | from reverso_api.voice import ReversoVoiceAPI 6 | from pprint import pprint 7 | import random 8 | 9 | 10 | print("Reverso.voice API usage example") 11 | 12 | print() 13 | api = ReversoVoiceAPI() 14 | print("Available Voices:") 15 | voices = api.voices 16 | pprint(voices) 17 | print() 18 | 19 | english_voice = random.choice(voices[random.choice(("Australian English", 20 | "British", 21 | "Indian English", 22 | "US English"))]) 23 | chinese_voice = random.choice(voices[random.choice(("Mandarin Chinese",))]) 24 | 25 | print("And now let's say something. English voice is {} and Chinese voice is {}".format(english_voice, 26 | chinese_voice)) 27 | for data in [("The phrase", english_voice), 28 | ("能让我来照顾你的小猫咪吗?", chinese_voice, 85), 29 | ("is translated from Chinese like", english_voice), 30 | ("Can I adopt your little kitten?", english_voice) 31 | ]: 32 | print(data[0]) 33 | api.say(*data, wait=True) -------------------------------------------------------------------------------- /tests/test_voice.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import contextlib 3 | 4 | from reverso_api.voice import ReversoVoiceAPI, Voice, get_voices 5 | 6 | 7 | def ask(what): 8 | while True: 9 | i = input("\n" + what + " [Y/N] ").upper() 10 | if i.startswith("Y"): 11 | return True 12 | elif i.startswith("N"): 13 | return False 14 | 15 | class TestReversoVoiceAPI(unittest.TestCase): 16 | api = ReversoVoiceAPI("Hello, World!", "Heather22k") 17 | 18 | def test__get_voices(self): 19 | voices = get_voices() 20 | self.assertTrue(isinstance(voices, dict)) 21 | for k, v in voices.items(): 22 | self.assertTrue(isinstance(k, str)) 23 | self.assertTrue(isinstance(v, list)) 24 | for voice in v: 25 | self.assertTrue(isinstance(voice, Voice)) 26 | 27 | def test__mp3_data(self): 28 | voices = get_voices() 29 | for voice in (voices["US English"][0], "Heather22k"): 30 | self.api.voice = voice 31 | mp3_data = self.api.mp3_data 32 | self.assertTrue(isinstance(mp3_data, bytes)) 33 | with open("data/voice/hello_world_us.mp3", "rb") as fp: 34 | self.assertEqual(fp.read(), mp3_data) 35 | 36 | def test__write_to_file(self): 37 | pass 38 | 39 | def test__say(self): 40 | voices = get_voices() 41 | try: 42 | with contextlib.redirect_stdout(None): 43 | import pygame 44 | except ImportError: 45 | raise ImportError("\n.say(...) method cannot be checked without pygame." 46 | "Install it first.") 47 | for voice in [voices["US English"][0], "Heather22k"]: 48 | self.api.say(wait=True) 49 | self.assertTrue(ask("Have you heard the voice, played 2 times?")) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reverso-API 2 | ##### A Pythonic wrapper around Reverso's ([reverso.net](https://reverso.net)) API. 3 | 4 | ### About Reverso services 5 | [Reverso](https://reverso.net) is a really powerful tool for foreign languages learners. 6 | It provides many different services, including but not limited to these: 7 | + [Context](https://context.reverso.net) — translates words and complex phrases with usage examples 8 | + [Conjugator](https://conjugator.reverso.net) — verb conjugator 9 | + [Dictionary](https://dictionary.reverso.net) — a dictionary (both definitions and translations to other languages) 10 | + [Spell Checker](https://reverso.net/spell-checker) — a spell checker 11 | + [Translation](https://www.reverso.net/text_translation.aspx) — a translator that supports sentences, words and phrases 12 | 13 | Another great feature of this website is its own text-to-speech engine for plenty of languages. ([Reverso Voice](https://voice.reverso.net/RestPronunciation.svc/help); 14 | [here](https://voice.reverso.net/RestPronunciation.svc/v1/output=json/GetVoiceStream/voiceName=Heather22k?inputText=VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgdGV4dCwgc3Bva2VuIGJ5IFJldmVyc28gVm9pY2U=) is an example of text, spoken by it). 15 | 16 | ### About this wrapper 17 | Currently, Context and Voice services are supported. 18 | 19 | #### Contributing 20 | I keep working on this project, adding new features and fixing bugs every couple of weeks. 21 | If you want to help me, file/respond to an issue or create a PR request! Any contributions would be greatly appreciated! 22 | 23 | ### Getting started 24 | 25 | #### Installation 26 | You have to install the package via pip. Just type this in the terminal/command line: 27 | ``` 28 | pip install reverso-api 29 | ``` 30 | 31 | #### Docs 32 | Docs are not ready yet. 33 | 34 | #### Examples 35 | There are some usage examples to help you figure out how to use this library. 36 | You can find them in the [examples](https://github.com/demian-wolf/ReversoAPI/tree/master/examples) 37 | project directory. 38 | 39 | **Keep in mind** that examples with text-to-speech require pygame to work! 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Pycharm IDE 141 | .idea 142 | -------------------------------------------------------------------------------- /examples/context/mini_reverso_context.py: -------------------------------------------------------------------------------- 1 | """Mini-version of Reverso Context with command-line interface.""" 2 | 3 | from reverso_api import ReversoContextAPI 4 | 5 | 6 | def highlight_example(text, highlighted): 7 | """'Highlights' ALL the highlighted parts of the word usage example with * characters. 8 | 9 | Args: 10 | text: The text of the example 11 | highlighted: Indexes of the highlighted parts' indexes 12 | 13 | Returns: 14 | The highlighted word usage example 15 | 16 | """ 17 | 18 | def insert_char(string, index, char): 19 | """Inserts the given character into a string. 20 | 21 | Example: 22 | string = "abc" 23 | index = 1 24 | char = "+" 25 | Returns: "a+bc" 26 | 27 | Args: 28 | string: Given string 29 | index: Index where to insert 30 | char: Which char to insert 31 | 32 | Return: 33 | String string with character char inserted at index index. 34 | """ 35 | 36 | return string[:index] + char + string[index:] 37 | 38 | def highlight_once(string, start, end, shift): 39 | """'Highlights' ONE highlighted part of the word usage example with two * characters. 40 | 41 | Example: 42 | string = "This is a sample string" 43 | start = 0 44 | end = 4 45 | shift = 0 46 | Returns: "*This* is a sample string" 47 | 48 | Args: 49 | string: The string to be highlighted 50 | start: The start index of the highlighted part 51 | end: The end index of the highlighted part 52 | shift: How many highlighting chars were already inserted (to get right indexes) 53 | 54 | Returns: 55 | The highlighted string. 56 | 57 | """ 58 | 59 | s = insert_char(string, start + shift, "*") 60 | s = insert_char(s, end + shift + 1, "*") 61 | return s 62 | 63 | shift = 0 64 | for start, end in highlighted: 65 | text = highlight_once(text, start, end, shift) 66 | shift += 2 67 | return text 68 | 69 | 70 | api = ReversoContextAPI( 71 | input("Enter the source text to search... "), 72 | input("Enter the target text to search (optional)... "), 73 | input("Enter the source language code... "), 74 | input("Enter the target language code... ") 75 | ) 76 | 77 | print() 78 | print("Translations:") 79 | for source_word, translation, frequency, part_of_speech, inflected_forms in api.get_translations(): 80 | print(source_word, "==", translation) 81 | print("Frequency (how many word usage examples contain this word):", frequency) 82 | print("Part of speech:", part_of_speech if part_of_speech else "unknown") 83 | if inflected_forms: 84 | print("Inflected forms:", end=" ") 85 | print(", ".join(inflected_form.translation for inflected_form in inflected_forms)) 86 | print() 87 | 88 | print() 89 | print("Word Usage Examples:") 90 | for source, target in api.get_examples(): 91 | print(highlight_example(source.text, source.highlighted), "==", 92 | highlight_example(target.text, target.highlighted)) -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import types 3 | 4 | from reverso_api.context import * 5 | 6 | 7 | # TODO: refactor 8 | 9 | class TestReversoContextAPI(unittest.TestCase): 10 | """TestCase for ReversoContextAPI 11 | 12 | Includes tests for: 13 | -- .get_examples() 14 | -- .get_translations() 15 | """ 16 | 17 | api = ReversoContextAPI(source_text="Github", 18 | source_lang="en", 19 | target_lang="ru") 20 | 21 | def test__properties(self): 22 | """Tests the ReversoContextAPI properties: 23 | 24 | -- supported_langs 25 | -- source_text 26 | -- target_text 27 | -- source_lang 28 | -- target_lang 29 | -- total_pages 30 | """ 31 | 32 | pass 33 | 34 | def test__eq(self): 35 | """ 36 | Tests the equality of ReversoContextAPI instances (ReversoContextAPI.__eq__). 37 | 38 | -- tests the equality of instances with different source text 39 | -- tests the equality of instances with the same attributes after .__data["npages"] was modified 40 | """ 41 | 42 | api2 = ReversoContextAPI(source_text="hello", 43 | source_lang="en", 44 | target_lang="ru") 45 | api3 = ReversoContextAPI(source_text="Github", 46 | source_lang="en", 47 | target_lang="ru") 48 | 49 | self.assertFalse(api2 == api3) 50 | self.assertTrue(self.api == api3) 51 | 52 | # .__data["npages"] was modified in both self.api and api3 53 | next(self.api.get_examples()) 54 | next(api3.get_examples()) 55 | self.assertTrue(self.api == api3) 56 | 57 | def test__get_examples(self): 58 | """Tests the ReversoContextAPI.get_examples() method. 59 | 60 | -- tests the correctness of types 61 | -- tests attributes of related classes (WordUsageContext) 62 | -- tests the length of examples: must be 2 (one for source, and one for target text) 63 | -- tests the length of pairs of indexes (items of the context.highlighted) 64 | -- tests if 0 <= index <= len(context.text) is True for all indexes 65 | """ 66 | 67 | examples = self.api.get_examples() 68 | self.assertTrue(isinstance(examples, types.GeneratorType)) 69 | 70 | for example in examples: 71 | self.assertTrue(isinstance(example, tuple)) 72 | self.assertTrue(len(example) == 2) 73 | for context in example: 74 | # Tests the WordUsageContext class 75 | self.assertTrue(isinstance(context, WordUsageContext)) 76 | for attr in ("text", "highlighted"): 77 | self.assertTrue(hasattr(context, attr)) 78 | self.assertTrue(isinstance(context.text, str)) 79 | self.assertTrue(isinstance(context.highlighted, tuple)) 80 | 81 | for indexes in context.highlighted: 82 | self.assertTrue(isinstance(indexes, tuple)) 83 | self.assertTrue(len(indexes) == 2) 84 | for index in indexes: 85 | self.assertTrue(isinstance(index, int)) 86 | self.assertTrue(0 <= index <= len(context.text)) 87 | 88 | def test__get_translations(self): 89 | """Tests the ReversoContextAPI.get_translations() 90 | 91 | -- tests the correctness of types 92 | -- tests attributes of related classes (Translation, InflectedForm) 93 | """ 94 | 95 | translations = self.api.get_translations() 96 | self.assertTrue(isinstance(translations, types.GeneratorType)) 97 | 98 | for translation in translations: 99 | self.assertTrue(isinstance(translation, Translation)) 100 | self.assertTrue(len(translation) == 5) 101 | 102 | # Tests the Translation class 103 | for attr in ("source_word", "translation", 104 | "frequency", "part_of_speech", 105 | "inflected_forms"): 106 | self.assertTrue(hasattr(Translation, attr)) 107 | self.assertTrue(translation.source_word == self.api.source_text) 108 | self.assertTrue(isinstance(translation.translation, str)) 109 | self.assertTrue(isinstance(translation.frequency, int)) 110 | self.assertTrue(isinstance(translation.part_of_speech, str) \ 111 | or translation.part_of_speech is None) 112 | self.assertTrue(isinstance(translation.inflected_forms, tuple)) 113 | 114 | # Tests the InflectedForms class 115 | for inflected_form in translation.inflected_forms: 116 | self.assertTrue(isinstance(inflected_form, InflectedForm)) 117 | for attr in ("translation", "frequency"): 118 | self.assertTrue(hasattr(inflected_form, attr)) 119 | self.assertTrue(isinstance(inflected_form.translation, str)) 120 | self.assertTrue(isinstance(inflected_form.frequency, int)) 121 | -------------------------------------------------------------------------------- /reverso_api/voice.py: -------------------------------------------------------------------------------- 1 | """Reverso Voice (voice.reverso.net) API for Python""" 2 | 3 | import base64 4 | import contextlib 5 | import io 6 | from collections import namedtuple, defaultdict 7 | 8 | import requests 9 | 10 | __all__ = ["ReversoVoiceAPI", "Voice"] 11 | 12 | BASE_URL = "https://voice.reverso.net/RestPronunciation.svc/v1/output=json/" 13 | 14 | Voice = namedtuple("Voice", ("name", "language", "gender")) 15 | 16 | 17 | class ReversoVoiceAPI: 18 | """Class for Reverso Voice API (https://voice.reverso.net/) 19 | 20 | Attributes: 21 | text 22 | voice 23 | speed 24 | mp3_data 25 | 26 | Methods: 27 | write_to_file(file) 28 | say(wait=False) 29 | 30 | """ 31 | 32 | def __init__(self, text, voice, speed=100): 33 | self.__voices = self.__get_voices() # TODO: make a frozen dict 34 | self.__voice_names = [voice.name 35 | for voices in self.__voices.values() 36 | for voice in voices] 37 | 38 | self.__text, self.__voice, self.__speed = None, None, None 39 | self.text, self.voice, self.speed = text, voice, speed 40 | 41 | @staticmethod 42 | def __get_voices(): 43 | voices = defaultdict(list) 44 | 45 | response = requests.get(BASE_URL + "GetAvailableVoices") 46 | 47 | voices_json = response.json() 48 | for voice_json in voices_json["Voices"]: 49 | language_name = voice_json["Language"] 50 | name, langcode, gender = voice_json["Name"], int(voice_json["LangCode"]), voice_json["Gender"] 51 | voice = Voice(name, (langcode, language_name), gender) 52 | voices[language_name].append(voice) 53 | 54 | return dict(voices) 55 | 56 | @property 57 | def text(self): 58 | return self.__text 59 | 60 | @property 61 | def voice(self): 62 | return self.__voice 63 | 64 | @property 65 | def speed(self): 66 | return self.__speed 67 | 68 | @property 69 | def mp3_data(self): 70 | if self.__info_modified: 71 | self.__mp3_data = requests.get( 72 | BASE_URL + "GetVoiceStream/voiceName={}?voiceSpeed={}&inputText={}".format(self.voice, self.speed, 73 | base64.b64encode( 74 | self.text.encode()).decode())).content 75 | self.__info_modified = False 76 | return self.__mp3_data 77 | 78 | @property 79 | def voices(self): 80 | return self.__voices 81 | 82 | @text.setter 83 | def text(self, value): 84 | assert isinstance(value, str), "text must be a string" 85 | self.__text = value 86 | self.__info_modified = True 87 | 88 | @voice.setter 89 | def voice(self, value): 90 | if isinstance(value, Voice): 91 | value = value.name 92 | assert value in self.__voice_names, "invalid voice" 93 | self.__voice = value 94 | self.__info_modified = True 95 | 96 | @speed.setter 97 | def speed(self, value): 98 | assert isinstance(value, int), "speed must be an integer" 99 | assert 30 <= value <= 300, "speed must be 30 <= speed <= 300" 100 | self.__speed = value 101 | self.__info_modified = True 102 | 103 | def write_to_file(self, file): 104 | """Writes the spoken phrase to an MP3 file. You can specify either a filename-string or a file-like object. 105 | If you are trying to pass another object as a file argument, TypeError is raised. 106 | 107 | Args: 108 | file: The output file (both strings with filenames and file-like objects are supported) 109 | 110 | Returns: 111 | none 112 | 113 | Raises: 114 | TypeError: if not filename (string) or a file-like object is passed as a file argument. 115 | 116 | """ 117 | 118 | if isinstance(file, str): 119 | with open(file, "wb") as fp: 120 | fp.write(self.mp3_data) 121 | return 122 | if hasattr(file, "write"): 123 | file.write(self.mp3_data) 124 | return 125 | raise TypeError("string or file-like object is required instead of {}".format(type(file))) 126 | 127 | def say(self, wait=False): 128 | """Reads the given text aloud. The difference from other methods is that this one PLAYS 129 | the sound (you can hear it if sound is enabled in your OS and pygame is installed). 130 | 131 | Note: 132 | Pygame is necessary for this method to work! You should install it before calling this 133 | method, otherwise ImportError will be raised. 134 | 135 | Args: 136 | wait: Tells whether it's necessary to wait until the text is fully spoken 137 | 138 | Returns: 139 | none 140 | 141 | Raises: 142 | ImportError: if Pygame is not installed 143 | 144 | """ 145 | 146 | try: 147 | with contextlib.redirect_stdout(None): # to remove the "Hello from the pygame community..." message 148 | import pygame 149 | except ImportError: 150 | raise ImportError("pygame is required for playing mp3 files, so you should install it first") 151 | 152 | pygame.mixer.init() 153 | with io.BytesIO() as fp: 154 | self.write_to_file(fp) 155 | fp.seek(0) 156 | pygame.mixer.music.load(fp) 157 | pygame.mixer.music.play() 158 | if wait: 159 | while pygame.mixer.music.get_busy(): 160 | pygame.time.delay(100) 161 | -------------------------------------------------------------------------------- /reverso_api/context.py: -------------------------------------------------------------------------------- 1 | """Reverso Context (context.reverso.net) API for Python""" 2 | 3 | import json 4 | from collections import namedtuple 5 | from typing import Generator 6 | 7 | import requests 8 | from bs4 import BeautifulSoup 9 | 10 | __all__ = ["ReversoContextAPI", "WordUsageContext", "Translation", "InflectedForm"] 11 | 12 | HEADERS = {"User-Agent": "Mozilla/5.0", 13 | "Content-Type": "application/json; charset=UTF-8" 14 | } 15 | 16 | WordUsageContext = namedtuple("WordUsageContext", 17 | ("text", "highlighted")) 18 | 19 | Translation = namedtuple("Translation", 20 | ("source_word", "translation", "frequency", "part_of_speech", "inflected_forms")) 21 | 22 | InflectedForm = namedtuple("InflectedForm", 23 | ("translation", "frequency")) 24 | 25 | 26 | class ReversoContextAPI(object): 27 | """Class for Reverso Context API (https://context.reverso.net/) 28 | 29 | Attributes: 30 | supported_langs 31 | source_text 32 | target_text 33 | source_lang 34 | target_lang 35 | total_pages 36 | 37 | Methods: 38 | get_translations() 39 | get_examples() 40 | swap_langs() 41 | """ 42 | 43 | def __init__(self, 44 | source_text="пример", 45 | target_text="", 46 | source_lang="ru", 47 | target_lang="en") -> None: 48 | 49 | self.__data = dict.fromkeys(("source_text", "target_text", "source_lang", "target_lang")) 50 | self.__total_pages = None 51 | self.__data_ismodified = True 52 | 53 | # FIXME: make self.supported_langs read-only 54 | self.supported_langs = self.__get_supported_langs() 55 | 56 | self.source_text, self.target_text = source_text, target_text 57 | self.source_lang, self.target_lang = source_lang, target_lang 58 | 59 | def __repr__(self) -> str: 60 | return ("ReversoContextAPI({0.source_text!r}, {0.target_text!r}, " 61 | "{0.source_lang!r}, {0.target_lang!r})").format(self) 62 | 63 | def __eq__(self, other) -> bool: 64 | if not isinstance(other, ReversoContextAPI): 65 | return False 66 | 67 | for attr in ("source_text", "target_text", "source_lang", "target_lang"): 68 | if getattr(self, attr) != getattr(other, attr): 69 | return False 70 | return True 71 | 72 | @staticmethod 73 | def __get_supported_langs() -> dict: 74 | supported_langs = {} 75 | 76 | response = requests.get("https://context.reverso.net/translation/", 77 | headers=HEADERS) 78 | 79 | soup = BeautifulSoup(response.content, features="lxml") 80 | 81 | src_selector = soup.find("div", id="src-selector") 82 | trg_selector = soup.find("div", id="trg-selector") 83 | 84 | for selector, attribute in ((src_selector, "source_lang"), 85 | (trg_selector, "target_lang")): 86 | dd_spans = selector.find(class_="drop-down").find_all("span") 87 | langs = [span.get("data-value") for span in dd_spans] 88 | langs = [lang for lang in langs 89 | if isinstance(lang, str) and len(lang) == 2] 90 | 91 | supported_langs[attribute] = tuple(langs) 92 | 93 | return supported_langs 94 | 95 | @property 96 | def source_text(self) -> str: 97 | return self.__data["source_text"] 98 | 99 | @property 100 | def target_text(self) -> str: 101 | return self.__data["target_text"] 102 | 103 | @property 104 | def source_lang(self) -> str: 105 | return self.__data["source_lang"] 106 | 107 | @property 108 | def target_lang(self) -> str: 109 | return self.__data["target_lang"] 110 | 111 | @property 112 | def total_pages(self) -> int: 113 | if self.__data_ismodified: 114 | response = requests.post("https://context.reverso.net/bst-query-service", 115 | headers=HEADERS, 116 | data=json.dumps(self.__data)) 117 | 118 | total_pages = response.json()["npages"] 119 | 120 | if not isinstance(total_pages, int): 121 | try: 122 | total_pages = int(total_pages) 123 | except ValueError: 124 | raise ValueError('"npages" in the response cannot be interpreted as an integer') 125 | if total_pages < 0: 126 | raise ValueError('"npages" in the response is a negative number') 127 | 128 | self.__total_pages = total_pages 129 | self.__data_ismodified = False 130 | 131 | return self.__total_pages 132 | 133 | def get_translations(self) -> Generator[Translation, None, None]: 134 | """ 135 | Yields all available translations for the word. 136 | 137 | While viewing Reverso.Context in a web browser, you can see them in a line just before the examples. 138 | 139 | Yields: 140 | Translation namedtuples. 141 | """ 142 | 143 | response = requests.post("https://context.reverso.net/bst-query-service", 144 | headers=HEADERS, 145 | data=json.dumps(self.__data)) 146 | translations_json = response.json()["dictionary_entry_list"] 147 | 148 | for translation_json in translations_json: 149 | translation = translation_json["term"] 150 | frequency = translation_json["alignFreq"] 151 | part_of_speech = translation_json["pos"] 152 | 153 | inflected_forms = tuple(InflectedForm(form["term"], form["alignFreq"]) 154 | for form in translation_json["inflectedForms"]) 155 | 156 | yield Translation(self.__data["source_text"], 157 | translation, frequency, part_of_speech, 158 | inflected_forms) 159 | 160 | def get_examples(self) -> Generator[tuple, None, None]: 161 | """A generator that gets words' usage examples pairs from server pair by pair. 162 | 163 | Note: 164 | Don't try to get all usage examples at one time if there are more than 5 pages (see the total_pages attribute). It 165 | may take a long time to complete because it will be necessary to connect to the server as many times as there are pages exist. 166 | Just get the usage examples one by one as they are being fetched. 167 | 168 | Yields: 169 | Tuples with two WordUsageContext namedtuples (for source and target text and highlighted indexes) 170 | """ 171 | 172 | def find_highlighted_idxs(soup, tag="em") -> tuple: 173 | """Finds indexes of the parts of the soup surrounded by a particular HTML tag 174 | relatively to the soup without the tag. 175 | 176 | Example: 177 | soup = BeautifulSoup("This is a sample string") 178 | tag = "em" 179 | Returns: [(0, 4), (8, 16)] 180 | 181 | Args: 182 | soup: The BeautifulSoup's soup. 183 | tag: The HTML tag, which surrounds the parts of the soup. 184 | 185 | Returns: 186 | A list of the tuples, which contain start and end indexes of the soup parts, 187 | surrounded by tags. 188 | 189 | """ 190 | 191 | # source: https://stackoverflow.com/a/62027247/8661764 192 | 193 | cur, idxs = 0, [] 194 | for t in soup.find_all(text=True): 195 | if t.parent.name == tag: 196 | idxs.append((cur, cur + len(t))) 197 | cur += len(t) 198 | return tuple(idxs) 199 | 200 | for npage in range(1, self.total_pages + 1): 201 | self.__data["npage"] = npage 202 | 203 | response = requests.post("https://context.reverso.net/bst-query-service", 204 | headers=HEADERS, 205 | data=json.dumps(self.__data)) 206 | examples_json = response.json()["list"] 207 | 208 | for example in examples_json: 209 | source = BeautifulSoup(example["s_text"], features="lxml") 210 | target = BeautifulSoup(example["t_text"], features="lxml") 211 | yield (WordUsageContext(source.text, find_highlighted_idxs(source)), 212 | WordUsageContext(target.text, find_highlighted_idxs(target))) 213 | 214 | @source_text.setter 215 | def source_text(self, value) -> None: 216 | self.__data["source_text"] = str(value) 217 | self.__data_ismodified = True 218 | 219 | @target_text.setter 220 | def target_text(self, value) -> None: 221 | self.__data["target_text"] = str(value) 222 | self.__data_ismodified = True 223 | 224 | @source_lang.setter 225 | def source_lang(self, value) -> None: 226 | value = str(value) 227 | 228 | if value not in self.supported_langs["source_lang"]: 229 | raise ValueError(f"{value!r} source language is not supported") 230 | 231 | if value == self.source_lang: 232 | raise ValueError(f"source language cannot be equal to the target language") 233 | 234 | self.__data["source_lang"] = value 235 | self.__data_ismodified = True 236 | 237 | @target_lang.setter 238 | def target_lang(self, value) -> None: 239 | value = str(value) 240 | 241 | if value not in self.supported_langs["target_lang"]: 242 | raise ValueError(f"{value!r} target language is not supported") 243 | 244 | if value == self.source_lang: 245 | raise ValueError(f"target language cannot be equal to the source language") 246 | 247 | self.__data["target_lang"] = value 248 | self.__data_ismodified = True 249 | 250 | def swap_langs(self) -> None: 251 | self.__data["source_lang"], self.__data["target_lang"] = self.__data["target_lang"], \ 252 | self.__data["source_lang"] 253 | self.__data_ismodified = True 254 | --------------------------------------------------------------------------------