├── 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 |
--------------------------------------------------------------------------------