├── .github └── workflows │ └── release.yml ├── .gitignore ├── EgyBestAPI └── __init__.py ├── LICENSE ├── README.md └── setup.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🆕️ Build & Release - PyPI 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-release: 11 | name: 🐍 Build Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: 1️⃣ Set Up Python 16 | uses: actions/setup-python@v3 17 | with: 18 | python-version: '3.x' 19 | - name: 2️⃣ Install Dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install build 23 | - name: 3️⃣ Build Package 24 | run: python -m build 25 | - name: 4️⃣ Save Artifacts 26 | uses: actions/upload-artifact@v3 27 | with: 28 | name: paillier-dist 29 | path: ./dist 30 | 31 | publish-release: 32 | name: 📦 Publish Release To PyPI 33 | needs: [build-release] 34 | runs-on: ubuntu-latest 35 | 36 | # upload to PyPI only on release 37 | if: github.event.release && github.event.action == 'published' 38 | steps: 39 | - uses: actions/download-artifact@v3 40 | with: 41 | name: paillier-dist 42 | path: dist 43 | 44 | - uses: pypa/gh-action-pypi-publish@v1.5.0 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 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 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /EgyBestAPI/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Py-EgyBest-Api 3 | ~~~~~~~~~ 4 | :Copyright: (c) 2022 By Amine Soukara . 5 | :License: MIT, See LICENSE For More Details. 6 | :Link: https://github.com/AmineSoukara/Py-EgyBest-Api 7 | :Description: Asynchronous Python Wrapper For EgyBest-API. 8 | """ 9 | 10 | 11 | import re 12 | from asyncio.exceptions import TimeoutError 13 | from json.decoder import JSONDecodeError 14 | 15 | import requests 16 | from dotmap import DotMap 17 | from requests.exceptions import ConnectionError 18 | from urllib3.exceptions import MaxRetryError, NewConnectionError 19 | 20 | # from requests.exceptions import JSONDecodeError 21 | 22 | 23 | class InvalidAccessToken(Exception): 24 | pass 25 | 26 | 27 | class RateLimitExceeded(Exception): 28 | pass 29 | 30 | 31 | class UserBanned(Exception): 32 | pass 33 | 34 | 35 | class LoginError(Exception): 36 | pass 37 | 38 | 39 | class ApiConnectionError(Exception): 40 | pass 41 | 42 | 43 | class RaEye: 44 | """ 45 | RaEye Class To Access All The Endpoints Of API. 46 | ___________ 47 | Parameters: 48 | RaEye(API_URL: str, ACCESS_TOKEN: str) 49 | """ 50 | 51 | def __init__( 52 | self, api_url: str, id: int = None, password: str = None, access_token: str = "", timeout: int = 60 53 | ): 54 | self.api_url = self.format_api_url(api_url.strip(" /")) 55 | self.refresh_token = None 56 | self.timeout = timeout 57 | 58 | if id and password: 59 | self.access_token, self.refresh_token = self.get_tokens(id, password) 60 | else: 61 | self.access_token = access_token 62 | 63 | def format_api_url(self, url): 64 | if not re.match("(?:http|ftp|https)://", url): 65 | return "http://{}".format(url) 66 | return url 67 | 68 | def get_tokens(self, id, password): 69 | try: 70 | body = {"id": id, "password": password} 71 | 72 | url = f"{self.api_url}/login/app" 73 | resp = requests.post(url, json=body, timeout=15) 74 | 75 | if resp.status_code == 403: 76 | raise UserBanned("Congrats, Your Are Banned (Forever)") 77 | 78 | response = resp.json() 79 | 80 | except ( 81 | TimeoutError, 82 | ConnectionError, 83 | MaxRetryError, 84 | NewConnectionError, 85 | JSONDecodeError, 86 | ): 87 | raise ApiConnectionError("Failed To Communicate With RaEye Server.") 88 | 89 | x = DotMap(response) 90 | if x.success: 91 | return x.data.access_token, x.data.refresh_token 92 | else: 93 | raise LoginError(x.message) 94 | 95 | async def fetch(self, route, method="GET", **params): 96 | try: 97 | headers = { 98 | "Accept": "application/json", 99 | "Content-Type": "application/json", 100 | "Authorization": "Bearer " + self.access_token, 101 | } 102 | 103 | url = f"{self.api_url}/{route}" 104 | resp = requests.request( 105 | method, url, headers=headers, timeout=self.timeout, params=params 106 | ) 107 | 108 | if resp.status_code in (422, 401): 109 | raise InvalidAccessToken( 110 | "Invalid Access Token, Get An Access Token From @EgyBestAPIBot" 111 | ) 112 | elif resp.status_code == 429: 113 | raise RateLimitExceeded("Rate Limit Exceeded") 114 | elif resp.status_code == 403: 115 | raise UserBanned("Congrats, Your Are Banned (Forever)") 116 | 117 | response = resp.json() 118 | 119 | except ( 120 | TimeoutError, 121 | ConnectionError, 122 | MaxRetryError, 123 | NewConnectionError, 124 | JSONDecodeError, 125 | ): 126 | raise ApiConnectionError("Failed To Communicate With RaEye Server.") 127 | 128 | return DotMap(response) 129 | 130 | async def dls(self, url: str, version: int): 131 | """ 132 | Returns An Object. 133 | Info: إستخراج روابط التحميل و المشاهدة 134 | Parameters: 135 | url (str): Episode or Movie link 136 | version (int): 1,2 Return As list 3,4 As Dict, Default 1 137 | Returns: 138 | Result Object (str): Results Which You Can Access With Dot Notation 139 | """ 140 | 141 | if not isinstance(version, int): 142 | raise ApiConnectionError( 143 | f"Version Arg Must Be A Number Not {type(version)}" 144 | ) 145 | 146 | return await self.fetch("dls", url=url, v=version) 147 | 148 | async def info(self, url: str): 149 | """ 150 | Returns An Object. 151 | Info: إستخراج كافة المعلومات 152 | Parameters: 153 | url (str): Link (Show - Movie ...) 154 | Returns: 155 | Result Object (str): Results Which You Can Access With Dot Notation 156 | """ 157 | return await self.fetch("info", url=url) 158 | 159 | async def table(self, url: str): 160 | """ 161 | Returns An Object. 162 | Info: إستخراج المعلومات 163 | Parameters: 164 | url (str): Link (Show - Movie ...) 165 | Returns: 166 | Result Object (str): Results Which You Can Access With Dot Notation 167 | """ 168 | return await self.fetch("table", url=url) 169 | 170 | async def movietable(self, url: str): 171 | """ 172 | Returns An Object. 173 | Info: إستخراج كافة معلومات الفيلم مع روابط التحميل و المشاهدة 174 | Parameters: 175 | url (str): Movie Link 176 | Returns: 177 | Result Object (str): Results Which You Can Access With Dot Notation 178 | """ 179 | return await self.fetch("movietable", url=url) 180 | 181 | async def quality(self, url: str): 182 | """ 183 | Returns An Object. 184 | Info: إستخراج الجودة 185 | Parameters: 186 | url (str): Link (Show - Movie ...) 187 | Returns: 188 | Result Object (str): Results Which You Can Access With Dot Notation 189 | """ 190 | return await self.fetch("quality", url=url) 191 | 192 | async def rating_percent(self, url: str): 193 | """ 194 | Returns An Object. 195 | Info: إستخراج التقيم 196 | Parameters: 197 | url (str): Link (Show - Movie ...) 198 | Returns: 199 | Result Object (str): Results Which You Can Access With Dot Notation 200 | """ 201 | return await self.fetch("rating_percent", url=url) 202 | 203 | async def note(self, url: str): 204 | """ 205 | Returns An Object. 206 | Info: إستخراج الملاحظة 207 | Parameters: 208 | url (str): Link (Show - Movie ...) 209 | Returns: 210 | Result Object (str): Results Which You Can Access With Dot Notation 211 | """ 212 | return await self.fetch("note", url=url) 213 | 214 | async def is_page_unavailable(self, url: str): 215 | """ 216 | Returns An Object. 217 | Info: عرض الصفحة اذا كانت غير متوفرة 218 | Parameters: 219 | url (str): Link (Show - Movie ...) 220 | Returns: 221 | Result Object (str): Results Which You Can Access With Dot Notation 222 | """ 223 | return await self.fetch("check_page", url=url) 224 | 225 | async def story(self, url: str): 226 | """ 227 | Returns An Object. 228 | Info: إستخراج القصة 229 | Parameters: 230 | url (str): Link (Show - Movie ...) 231 | Returns: 232 | Result Object (str): Results Which You Can Access With Dot Notation 233 | """ 234 | return await self.fetch("story", url=url) 235 | 236 | async def trailer(self, url: str): 237 | """ 238 | Returns An Object. 239 | Info: إستخراج التريلر 240 | Parameters: 241 | url (str): Link (Show - Movie ...) 242 | Returns: 243 | Result Object (str): Results Which You Can Access With Dot Notation 244 | """ 245 | return await self.fetch("trailer", url=url) 246 | 247 | async def thumbnail(self, url: str): 248 | """ 249 | Returns An Object. 250 | Info: إستخراج الصورة 251 | Parameters: 252 | url (str): Link (Show - Movie ...) 253 | Returns: 254 | Result Object (str): Results Which You Can Access With Dot Notation 255 | """ 256 | return await self.fetch("thumbnail", url=url) 257 | 258 | async def title(self, url: str): 259 | """ 260 | Returns An Object. 261 | Info: إستخراج العنوان 262 | Parameters: 263 | url (str): Link (Show - Movie ...) 264 | Returns: 265 | Result Object (str): Results Which You Can Access With Dot Notation 266 | """ 267 | return await self.fetch("title", url=url) 268 | 269 | async def similar(self, url: str): 270 | """ 271 | Returns An Object. 272 | Info: إستخراج الافلام المتشابهة 273 | Parameters: 274 | url (str): Link (Movie) 275 | Returns: 276 | Result Object (str): Results Which You Can Access With Dot Notation 277 | """ 278 | return await self.fetch("similar", url=url) 279 | 280 | async def actors(self, url: str): 281 | """ 282 | Returns An Object. 283 | Info: إستخراج معلومات الممثلين 284 | Parameters: 285 | url (str): Link (Show - Movie ...) 286 | Returns: 287 | Result Object (str): Results Which You Can Access With Dot Notation 288 | """ 289 | return await self.fetch("actors", url=url) 290 | 291 | async def search(self, query: str, type: str): 292 | """ 293 | Returns An Object. 294 | Info: البحث المتطور 295 | Parameters: 296 | query (str): title (Show - Movie ...) 297 | type (str): types (all-serie-movie-anime-show) default all 298 | Returns: 299 | Result Object (str): Results Which You Can Access With Dot Notation 300 | """ 301 | return await self.fetch("search", query=query, type=type) 302 | 303 | async def pages(self, path: str, limit: int): 304 | """ 305 | Returns An Object. 306 | Info: استخراج المعطيات من عدة صفحات من فرع محدد 307 | Parameters: 308 | path (str): paths (movies/top - movies/latest ...) 309 | limit (int): number of pages default 1 310 | Returns: 311 | Result Object (str): Results Which You Can Access With Dot Notation 312 | """ 313 | 314 | if not isinstance(limit, int): 315 | raise ApiConnectionError(f"Limit Arg Must Be A Number Not {type(limit)}") 316 | 317 | return await self.fetch("pages", path=path, limit=limit) 318 | 319 | async def page(self, path: str, number: int): 320 | """ 321 | Returns An Object. 322 | Info: استخراج المعطيات من صفحة محددة من فرع محدد 323 | Parameters: 324 | path (str): paths (movies/top - movies/latest ...) 325 | number (int): page number default 1 326 | Returns: 327 | Result Object (str): Results Which You Can Access With Dot Notation 328 | """ 329 | 330 | if not isinstance(number, int): 331 | raise ApiConnectionError(f"Number Arg Must Be A Number Not {type(number)}") 332 | 333 | return await self.fetch("page", path=path, number=number) 334 | 335 | async def seasons(self, url: str): 336 | """ 337 | Returns An Object. 338 | Info: إستخراج المواسم من مسلسل معين 339 | Parameters: 340 | url (str): Link (Show - Serie) 341 | Returns: 342 | Result Object (str): Results Which You Can Access With Dot Notation 343 | """ 344 | return await self.fetch("seasons", url=url) 345 | 346 | async def episodes(self, url: str): 347 | """ 348 | Returns An Object. 349 | Info: إستخراج الحلقات من موسم معين 350 | Parameters: 351 | url (str): Season Link (Show-Serie) 352 | Returns: 353 | Result Object (str): Results Which You Can Access With Dot Notation 354 | """ 355 | return await self.fetch("episodes", url=url) 356 | 357 | async def previous_next(self, url: str): 358 | """ 359 | Returns An Object. 360 | Info: إستخراج الحلقة السابقة و التالية لحلقة معينة 361 | Parameters: 362 | url (str): Episode Link (Show-Serie) 363 | Returns: 364 | Result Object (str): Results Which You Can Access With Dot Notation 365 | """ 366 | return await self.fetch("previous_next", url=url) 367 | 368 | async def maintenance(self): 369 | """ 370 | Returns An Object. 371 | Info: عرض إذا كان أيبي تحت الصيانة 372 | Parameters: 373 | None 374 | Returns: 375 | Result Object (str): Results Which You Can Access With Dot Notation 376 | """ 377 | return await self.fetch("maintenance") 378 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 DAMIEN 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RUN](https://telegra.ph/file/39890dfb899f4a943e626.jpg) 2 | 3 |
4 | 5 | > Asynchronous Python Wrapper For EgyBest-API. 6 | 7 | 8 |

9 | 10 | contributors 11 | 12 | 13 | last update 14 | 15 | 16 | forks 17 | 18 | 19 | stars 20 | 21 | 22 | open issues 23 | 24 | 25 | license 26 | 27 | 28 | Total Downloads 29 | 30 | 31 | hits 32 | 33 |

34 | 35 | 36 | 37 |

38 | API Repository 39 | · 40 | Documentation 41 | · 42 | Report Bug 43 | · 44 | Request Feature 45 |

46 |
47 | 48 | ## 49 | 50 |
51 | 52 | # ⚠️ Note: 53 | - For Personal Use Only, Don't Create Or Build Something Huge With This API Without Permission Otherwise You Will Be Banned 54 | - This Project Is Licensed Under The [MIT License](https://github.com/AmineSoukara/Py-EgyBest-Api/blob/main/LICENSE) 55 | 56 | ## 57 | 58 | # 🔐 Requirements: 59 | 60 | - Python 3.8 Or Newer. 61 | - API URL and Access Token From : 62 | 63 | 64 | ## 65 | 66 | # 🗜 Installation: 67 | 68 | ```sh 69 | $ pip3 install Py-EgyBest-Api 70 | ``` 71 | ## 72 | 73 | # ❓ Usage: 74 | For Example, To Get Direct Links, You Can Do This 75 |
76 | 77 | ```py 78 | from EgyBestAPI import RaEye 79 | from asyncio import run 80 | 81 | API = "http://0.1.2.3" # Required 82 | TOKEN = "abcd" # Required 83 | ID = None # Optional 84 | PASSWORD = None # Optional 85 | 86 | x = RaEye(API, access_token=TOKEN, id=ID, password=PASSWORD) 87 | 88 | async def main(): 89 | z = await x.dls(url="https://www.egybest.org/movie/top-five-2014", version=2) 90 | 91 | print(z) 92 | # output: DotMap(a=1, b=2) 93 | print(z.type) 94 | # output: movie 95 | 96 | print(z.toDict()) 97 | # output: {'a': 1, 'b': 2} 98 | print(z["type"]) 99 | # output: movie 100 | 101 | # To Print Results As Json 102 | z.pprint(pformat='json') 103 | # output: {...} 104 | 105 | run(main()) 106 | ``` 107 | 108 |
109 | 110 | ## 111 | 112 | # 🪐 Documentation: 113 | There Is No Documentation, But You Can Take Help From The DocStrings : 114 | 115 |
116 | 117 | ```py 118 | from EgyBestAPI import RaEye 119 | 120 | print(help(RaEye.dls)) 121 | ``` 122 | 123 |
124 | 125 | ## 126 | 127 | # ⭐️ Features: 128 |
129 | 130 | * [x] Smart Search 131 | * [x] Extract Full Info From Url (Movie-Serie/Anime) 132 | * [x] Extract: Story - Image - Title - Trailer - Actors Info - Note 133 | * [x] Extract Download and Stream Links With Full Info 134 | * [x] Extract Similar Movies 135 | * [x] Extract Seasons / Episodes 136 | * [x] Extract Previous And Next Episode 137 | * [x] Extract Movies Or Series From Paths 138 | * [x] And More ... 139 | 140 |
141 | 142 | ## 143 | 144 | # 🧭 RoadMap: 145 |
146 | 147 | * [ ] Todo 148 | 149 |
150 | 151 | ## 152 | 153 | # 👨‍💻 Dev & Support: 154 | 155 | 156 | 157 | 158 | ## 159 | 160 | # 📭 Credits: 161 | 162 | 163 | ## 164 | 165 | # 🌍 Contributing: 166 | 167 | 168 | 169 | 170 | 171 | Contributions Are Always Welcome! 172 | See `contributing.md` For Ways To Get Started. 173 | 174 | ## 175 | 176 | ![⭐️](https://telegra.ph/file/b132a131aabe2106bd335.gif) 177 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Py-EgyBest-Api 3 | ~~~~~~~~~ 4 | :Copyright: (c) 2022 By Amine Soukara . 5 | :License: MIT, See LICENSE For More Details. 6 | :Link: https://github.com/AmineSoukara/Py-EgyBest-Api 7 | :Description: Asynchronous Python Wrapper For EgyBest-API. 8 | """ 9 | 10 | from setuptools import find_packages, setup 11 | import datetime 12 | 13 | 14 | AUTHOR = "AmineSoukara" 15 | EMAIL = "AmineSoukara@gmail.com" 16 | URL = "https://github.com/AmineSoukara/Py-EgyBest-Api" 17 | 18 | 19 | # Get the long description 20 | with open("README.md", "r", encoding="utf-8") as fh: 21 | long_description = fh.read() 22 | 23 | today_date = datetime.date.today() 24 | VERSION = today_date.strftime("%-y.%-m.%-d") 25 | # VERSION = '1.7' 26 | 27 | setup( 28 | name="Py-EgyBest-Api", 29 | version=VERSION, 30 | description="Asynchronous Python Wrapper For EgyBest-API.", 31 | long_description=long_description, 32 | long_description_content_type="text/markdown", 33 | author=AUTHOR, 34 | author_email=EMAIL, 35 | url=URL, 36 | license="MIT", 37 | packages=find_packages(), 38 | keywords="EgyBest Api Scrapper Python", 39 | project_urls={ 40 | "Source": "https://github.com/AmineSoukara/Py-EgyBest-Api", 41 | "Documentation": "https://github.com/AmineSoukara/Py-EgyBest-Api#readme", 42 | "Tracker": "https://github.com/AmineSoukara/Py-EgyBest-Api/issues", 43 | }, 44 | classifiers=[ 45 | "Development Status :: 5 - Production/Stable", 46 | "Intended Audience :: Developers", 47 | "Topic :: Software Development :: Build Tools", 48 | "Natural Language :: English", 49 | "License :: OSI Approved :: MIT License", 50 | "Operating System :: OS Independent", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Topic :: Internet", 54 | ], 55 | python_requires=">=3.8", 56 | install_requires=["aiohttp", "aiofiles", "dotmap", "requests", "urllib3"], 57 | ) 58 | --------------------------------------------------------------------------------