diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1b6745c..2f99f1e 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,26 +1,79 @@ -name: Upload Python Package +name: CI and Publish on: + push: + branches: [ "typing" ] # тестирование при пуше в ветку typing + pull_request: + branches: [ "main" ] # тестирование при PR в main release: - types: [published] + types: [published] # публикация только при создании релиза jobs: + test: + name: Test on Python ${{ matrix.python-version }} + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + # Для тестирования бета/альфа версий можно добавить: + # include: + # - python-version: "3.14-dev" # тестирование на dev версии + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 # обновил до v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true # разрешает установку pre-release версий (например, 3.14-dev) + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + # Устанавливаем pytest для более детальных отчётов + pip install pytest pytest-cov + # Устанавливаем ваш пакет в режиме разработки + pip install -e . + pip install -r requirements.txt + + - name: Run tests with pytest + run: | + pytest tests/ --cov=./ --cov-report=xml -v + + - name: Upload coverage to Codecov (опционально) + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + fail_ci_if_error: false + deploy: + needs: test runs-on: ubuntu-latest + # Публикация только при создании релиза (и после успешных тестов) + if: github.event_name == 'release' && github.event.action == 'published' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Install dependencies + + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install build + pip install build twine # twine для дополнительной проверки + - name: Build package run: python -m build - - name: Publish package + + - name: Check package with twine + run: twine check dist/* + + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} + # Если хотите тестовую публикацию в Test PyPI: + # repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/README.md b/README.md index abec742..5885da4 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,9 @@ Python Telegraph API wrapper ```bash $ python3 -m pip install telegraph -# with asyncio support -$ python3 -m pip install 'telegraph[aio]' ``` -## Example +### Example ```python from telegraph import Telegraph @@ -25,9 +23,23 @@ response = telegraph.create_page( html_content='
Hello, world!
' ) print(response['url']) + +telegraph.close() +``` + +#### Or with context manager +```python + +with Telegraph() as telegraph: + telegraph.create_account(short_name='1337') + response = telegraph.create_page( + 'Hey', + html_content='Hello, world!
' + ) + print(response['url']) ``` -## Async Example +### Async Example ```python import asyncio from telegraph.aio import Telegraph @@ -42,6 +54,26 @@ async def main(): ) print(response['url']) + await telegraph.aclose() + + +asyncio.run(main()) +``` + +#### Or with context manager +```python +import asyncio +from telegraph.aio import Telegraph + +async def main(): + async with Telegraph() as telegraph: + print(await telegraph.create_account(short_name='1337')) + + response = await telegraph.create_page( + 'Hey', + html_content='Hello, world!
', + ) + print(response['url']) asyncio.run(main()) ``` diff --git a/docs/conf.py b/docs/conf.py index f2611ff..7d83395 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,8 @@ import os import sys -sys.path.insert(0, os.path.abspath('../')) + +sys.path.insert(0, os.path.abspath("../")) from datetime import datetime @@ -31,26 +32,23 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'telegraph' +project = "telegraph" copyright = '{:%Y}, {}'.format( - datetime.utcnow(), - telegraph.__author__ + datetime.utcnow(), telegraph.__author__ ) author = telegraph.__author__ @@ -65,15 +63,15 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -84,31 +82,31 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { - 'show_powered_by': False, - 'github_user': 'python273', - 'github_repo': 'telegraph', - 'github_banner': True, - 'github_type': 'star', - 'show_related': False + "show_powered_by": False, + "github_user": "python273", + "github_repo": "telegraph", + "github_banner": True, + "github_type": "star", + "show_related": False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'telegraphdoc' +htmlhelp_basename = "telegraphdoc" # -- Options for LaTeX output --------------------------------------------- @@ -117,15 +115,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -135,8 +130,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'telegraph.tex', 'telegraph Documentation', - 'Author', 'manual'), + (master_doc, "telegraph.tex", "telegraph Documentation", "Author", "manual"), ] @@ -144,10 +138,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'telegraph', 'telegraph Documentation', - [author], 1) -] +man_pages = [(master_doc, "telegraph", "telegraph Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -156,13 +147,18 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'telegraph', 'telegraph Documentation', - author, 'telegraph', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "telegraph", + "telegraph Documentation", + author, + "telegraph", + "One line description of project.", + "Miscellaneous", + ), ] - # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. @@ -181,6 +177,4 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - - +epub_exclude_files = ["search.html"] diff --git a/generate_async_api.py b/generate_async_api.py index 1009a5c..2ac6b7c 100644 --- a/generate_async_api.py +++ b/generate_async_api.py @@ -1,9 +1,9 @@ """Generate async api from sync api""" + from typing import Optional import libcst as cst from libcst._nodes.expression import Await -from libcst._nodes.whitespace import SimpleWhitespace class SyncToAsyncTransformer(cst.CSTTransformer): @@ -71,12 +71,14 @@ def leave_Call(self, original_node: cst.FunctionDef, updated_node: cst.FunctionD # await the call if it's API class method should_await = ( path[-2:] == ["session", "self"] - or path[-3:] == [ + or path[-3:] + == [ "method", "_telegraph", "self", ] - or path[-3:] == [ + or path[-3:] + == [ "upload_file", "_telegraph", "self", @@ -104,9 +106,7 @@ def leave_FunctionDef( return updated_node # mark fn as async - return updated_node.with_changes( - asynchronous=cst.Asynchronous() - ) + return updated_node.with_changes(asynchronous=cst.Asynchronous()) def main(): diff --git a/requirements.txt b/requirements.txt index b3f0581..27a1c34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -requests httpx libcst \ No newline at end of file diff --git a/setup.py b/setup.py index ab9a5c2..a01cccf 100755 --- a/setup.py +++ b/setup.py @@ -14,46 +14,39 @@ except ImportError: from distutils.core import setup -version = '2.2.0' +version = "2.2.0" -with open('README.md') as f: +with open("README.md") as f: long_description = f.read() setup( - name='telegraph', + name="telegraph", version=version, - - author='python273', - - author_email='telegraph@python273.pw', - url='https://github.com/python273/telegraph', - - description='Telegraph API wrapper', + author="python273", + author_email="telegraph@python273.pw", + url="https://github.com/python273/telegraph", + description="Telegraph API wrapper", long_description=long_description, - long_description_content_type='text/markdown', - - download_url='https://github.com/python273/telegraph/archive/v{}.zip'.format( + long_description_content_type="text/markdown", + download_url="https://github.com/python273/telegraph/archive/v{}.zip".format( version ), - license='MIT', - - packages=['telegraph'], - install_requires=['requests'], - extras_require={ - 'aio': ['httpx'], - }, - + license="MIT", + packages=["telegraph"], + install_requires=["httpx"], classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - ] + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + ], ) diff --git a/telegraph/__init__.py b/telegraph/__init__.py index 385e236..f9e0d9b 100644 --- a/telegraph/__init__.py +++ b/telegraph/__init__.py @@ -8,8 +8,14 @@ Copyright (C) 2018 """ -__author__ = 'python273' -__version__ = '2.2.0' +__author__ = "python273" +__version__ = "2.2.0" from .api import Telegraph, TelegraphException from .upload import upload_file + +__all__ = [ + "Telegraph", + "TelegraphException", + "upload_file", +] diff --git a/telegraph/aio.py b/telegraph/aio.py index 79c4f60..dc27837 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -1,14 +1,23 @@ # -*- coding: utf-8 -*- -import json + +from __future__ import annotations import httpx +from os import PathLike +from typing import Union, Optional, Any, List + +try: + from typing import BinaryIO +except ImportError: + from typing.io import BinaryIO + from .exceptions import TelegraphException, RetryAfterError from .utils import html_to_nodes, nodes_to_html, FilesOpener, json_dumps class TelegraphApi: - """ Telegraph API Client + """Telegraph API Client :param access_token: access_token :type access_token: str @@ -16,36 +25,37 @@ class TelegraphApi: :param domain: domain (e.g. alternative mirror graph.org) """ - __slots__ = ('access_token', 'domain', 'session') + __slots__ = ("access_token", "domain", "session") - def __init__(self, access_token=None, domain='telegra.ph'): + def __init__(self, access_token: Optional[str] = None, domain: str = "telegra.ph"): self.access_token = access_token self.domain = domain self.session = httpx.AsyncClient() - async def method(self, method, values=None, path=''): + async def method(self, method: str, values: Any = None, path: str = ""): values = values.copy() if values is not None else {} - if 'access_token' not in values and self.access_token: - values['access_token'] = self.access_token + if "access_token" not in values and self.access_token: + values["access_token"] = self.access_token - response = (await self.session.post( - 'https://api.{}/{}/{}'.format(self.domain, method, path), - data=values - )).json() + response = ( + await self.session.post( + "https://api.{}/{}/{}".format(self.domain, method, path), data=values + ) + ).json() - if response.get('ok'): - return response['result'] + if response.get("ok"): + return response["result"] - error = response.get('error') - if isinstance(error, str) and error.startswith('FLOOD_WAIT_'): - retry_after = int(error.rsplit('_', 1)[-1]) + error = response.get("error") + if isinstance(error, str) and error.startswith("FLOOD_WAIT_"): + retry_after = int(error.rsplit("_", 1)[-1]) raise RetryAfterError(retry_after) else: raise TelegraphException(error) - async def upload_file(self, f): - """ Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK + async def upload_file(self, f: Union[PathLike[str], BinaryIO]): + """Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK Returns a list of dicts with `src` key. Allowed only .jpg, .jpeg, .png, .gif and .mp4 files. @@ -53,19 +63,20 @@ async def upload_file(self, f): :type f: file, str or list """ with FilesOpener(f) as files: - response = (await self.session.post( - 'https://{}/upload'.format(self.domain), - files=files - )).json() + response = ( + await self.session.post( + "https://{}/upload".format(self.domain), files=files + ) + ).json() if isinstance(response, list): - error = response[0].get('error') + error = response[0].get("error") else: - error = response.get('error') + error = response.get("error") if error: - if isinstance(error, str) and error.startswith('FLOOD_WAIT_'): - retry_after = int(error.rsplit('_',1)[-1]) + if isinstance(error, str) and error.startswith("FLOOD_WAIT_"): + retry_after = int(error.rsplit("_", 1)[-1]) raise RetryAfterError(retry_after) else: raise TelegraphException(error) @@ -74,24 +85,29 @@ async def upload_file(self, f): class Telegraph: - """ Telegraph API client helper + """Telegraph API client helper :param access_token: access token :param domain: domain (e.g. alternative mirror graph.org) """ - __slots__ = ('_telegraph',) + __slots__ = ("_telegraph",) - def __init__(self, access_token=None, domain='telegra.ph'): + def __init__(self, access_token: Optional[str] = None, domain: str = "telegra.ph"): self._telegraph = TelegraphApi(access_token, domain) def get_access_token(self): """Get current access_token""" return self._telegraph.access_token - async def create_account(self, short_name, author_name=None, author_url=None, - replace_token=True): - """ Create a new Telegraph account + async def create_account( + self, + short_name: str, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + replace_token: bool = True, + ): + """Create a new Telegraph account :param short_name: Account name, helps users with several accounts remember which they are currently using. @@ -103,20 +119,27 @@ async def create_account(self, short_name, author_name=None, author_url=None, not necessarily to a Telegram profile or channels :param replace_token: Replaces current token to a new user's token """ - response = (await self._telegraph.method('createAccount', values={ - 'short_name': short_name, - 'author_name': author_name, - 'author_url': author_url - })) + response = await self._telegraph.method( + "createAccount", + values={ + "short_name": short_name, + "author_name": author_name, + "author_url": author_url, + }, + ) if replace_token: - self._telegraph.access_token = response.get('access_token') + self._telegraph.access_token = response.get("access_token") return response - async def edit_account_info(self, short_name=None, author_name=None, - author_url=None): - """ Update information about a Telegraph account. + async def edit_account_info( + self, + short_name: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + ): + """Update information about a Telegraph account. Pass only the parameters that you want to edit :param short_name: Account name, helps users with several @@ -128,44 +151,56 @@ async def edit_account_info(self, short_name=None, author_name=None, author's name below the title. Can be any link, not necessarily to a Telegram profile or channels """ - return (await self._telegraph.method('editAccountInfo', values={ - 'short_name': short_name, - 'author_name': author_name, - 'author_url': author_url - })) + return await self._telegraph.method( + "editAccountInfo", + values={ + "short_name": short_name, + "author_name": author_name, + "author_url": author_url, + }, + ) async def revoke_access_token(self): - """ Revoke access_token and generate a new one, for example, - if the user would like to reset all connected sessions, or - you have reasons to believe the token was compromised. - On success, returns dict with new access_token and auth_url fields + """Revoke access_token and generate a new one, for example, + if the user would like to reset all connected sessions, or + you have reasons to believe the token was compromised. + On success, returns dict with new access_token and auth_url fields """ - response = (await self._telegraph.method('revokeAccessToken')) + response = await self._telegraph.method("revokeAccessToken") - self._telegraph.access_token = response.get('access_token') + self._telegraph.access_token = response.get("access_token") return response - async def get_page(self, path, return_content=True, return_html=True): - """ Get a Telegraph page + async def get_page( + self, path: str, return_content: bool = True, return_html: bool = True + ): + """Get a Telegraph page :param path: Path to the Telegraph page (in the format Title-12-31, i.e. everything that comes after https://telegra.ph/) :param return_content: If true, content field will be returned :param return_html: If true, returns HTML instead of Nodes list """ - response = (await self._telegraph.method('getPage', path=path, values={ - 'return_content': return_content - })) + response = await self._telegraph.method( + "getPage", path=path, values={"return_content": return_content} + ) if return_content and return_html: - response['content'] = nodes_to_html(response['content']) + response["content"] = nodes_to_html(response["content"]) return response - async def create_page(self, title, content=None, html_content=None, - author_name=None, author_url=None, return_content=False): - """ Create a new Telegraph page + async def create_page( + self, + title: str, + content: List[Any] = None, + html_content: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + return_content: bool = False, + ): + """Create a new Telegraph page :param title: Page title :param content: Content in nodes list format (see doc) @@ -180,17 +215,28 @@ async def create_page(self, title, content=None, html_content=None, content_json = json_dumps(content) - return (await self._telegraph.method('createPage', values={ - 'title': title, - 'author_name': author_name, - 'author_url': author_url, - 'content': content_json, - 'return_content': return_content - })) - - async def edit_page(self, path, title, content=None, html_content=None, - author_name=None, author_url=None, return_content=False): - """ Edit an existing Telegraph page + return await self._telegraph.method( + "createPage", + values={ + "title": title, + "author_name": author_name, + "author_url": author_url, + "content": content_json, + "return_content": return_content, + }, + ) + + async def edit_page( + self, + path: str, + title: str, + content: List[Any] = None, + html_content: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + return_content: bool = False, + ): + """Edit an existing Telegraph page :param path: Path to the page :param title: Page title @@ -206,28 +252,32 @@ async def edit_page(self, path, title, content=None, html_content=None, content_json = json_dumps(content) - return (await self._telegraph.method('editPage', path=path, values={ - 'title': title, - 'author_name': author_name, - 'author_url': author_url, - 'content': content_json, - 'return_content': return_content - })) - - async def get_account_info(self, fields=None): - """ Get information about a Telegraph account + return await self._telegraph.method( + "editPage", + path=path, + values={ + "title": title, + "author_name": author_name, + "author_url": author_url, + "content": content_json, + "return_content": return_content, + }, + ) + + async def get_account_info(self, fields: Union[List[str], None] = None): + """Get information about a Telegraph account :param fields: List of account fields to return. Available fields: short_name, author_name, author_url, auth_url, page_count Default: [“short_name”,“author_name”,“author_url”] """ - return (await self._telegraph.method('getAccountInfo', { - 'fields': json_dumps(fields) if fields else None - })) + return await self._telegraph.method( + "getAccountInfo", {"fields": json_dumps(fields) if fields else None} + ) - async def get_page_list(self, offset=0, limit=50): - """ Get a list of pages belonging to a Telegraph account + async def get_page_list(self, offset: int = 0, limit: int = 50): + """Get a list of pages belonging to a Telegraph account sorted by most recently created pages first :param offset: Sequential number of the first page to be returned @@ -235,13 +285,19 @@ async def get_page_list(self, offset=0, limit=50): :param limit: Limits the number of pages to be retrieved (0-200, default = 50) """ - return (await self._telegraph.method('getPageList', { - 'offset': offset, - 'limit': limit - })) - - async def get_views(self, path, year=None, month=None, day=None, hour=None): - """ Get the number of views for a Telegraph article + return await self._telegraph.method( + "getPageList", {"offset": offset, "limit": limit} + ) + + async def get_views( + self, + path: str, + year: Union[int, None] = None, + month: Union[int, None] = None, + day: Union[int, None] = None, + hour: Union[int, None] = None, + ): + """Get the number of views for a Telegraph article :param path: Path to the Telegraph page :param year: Required if month is passed. If passed, the number of @@ -253,19 +309,28 @@ async def get_views(self, path, year=None, month=None, day=None, hour=None): :param hour: If passed, the number of page views for the requested hour will be returned """ - return (await self._telegraph.method('getViews', path=path, values={ - 'year': year, - 'month': month, - 'day': day, - 'hour': hour - })) - - async def upload_file(self, f): - """ Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK + return await self._telegraph.method( + "getViews", + path=path, + values={"year": year, "month": month, "day": day, "hour": hour}, + ) + + async def upload_file(self, f: Union[PathLike, BinaryIO]): + """Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK Returns a list of dicts with `src` key. Allowed only .jpg, .jpeg, .png, .gif and .mp4 files. :param f: filename or file-like object. :type f: file, str or list """ - return (await self._telegraph.upload_file(f)) + return await self._telegraph.upload_file(f) + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + await self.aclose() + + async def aclose(self): + if not self._telegraph.session.is_closed: + await self._telegraph.session.aclose() diff --git a/telegraph/api.py b/telegraph/api.py index f4c7569..683bea6 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -1,14 +1,23 @@ # -*- coding: utf-8 -*- -import json -import requests +from __future__ import annotations + +import httpx + +from os import PathLike +from typing import Union, Optional, Any, List + +try: + from typing import BinaryIO +except ImportError: + from typing.io import BinaryIO from .exceptions import TelegraphException, RetryAfterError from .utils import html_to_nodes, nodes_to_html, FilesOpener, json_dumps class TelegraphApi: - """ Telegraph API Client + """Telegraph API Client :param access_token: access_token :type access_token: str @@ -16,36 +25,35 @@ class TelegraphApi: :param domain: domain (e.g. alternative mirror graph.org) """ - __slots__ = ('access_token', 'domain', 'session') + __slots__ = ("access_token", "domain", "session") - def __init__(self, access_token=None, domain='telegra.ph'): + def __init__(self, access_token: Optional[str] = None, domain: str = "telegra.ph"): self.access_token = access_token self.domain = domain - self.session = requests.Session() + self.session = httpx.Client() - def method(self, method, values=None, path=''): + def method(self, method: str, values: Any = None, path: str = ""): values = values.copy() if values is not None else {} - if 'access_token' not in values and self.access_token: - values['access_token'] = self.access_token + if "access_token" not in values and self.access_token: + values["access_token"] = self.access_token response = self.session.post( - 'https://api.{}/{}/{}'.format(self.domain, method, path), - data=values + "https://api.{}/{}/{}".format(self.domain, method, path), data=values ).json() - if response.get('ok'): - return response['result'] + if response.get("ok"): + return response["result"] - error = response.get('error') - if isinstance(error, str) and error.startswith('FLOOD_WAIT_'): - retry_after = int(error.rsplit('_', 1)[-1]) + error = response.get("error") + if isinstance(error, str) and error.startswith("FLOOD_WAIT_"): + retry_after = int(error.rsplit("_", 1)[-1]) raise RetryAfterError(retry_after) else: raise TelegraphException(error) - def upload_file(self, f): - """ Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK + def upload_file(self, f: Union[PathLike[str], BinaryIO]): + """Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK Returns a list of dicts with `src` key. Allowed only .jpg, .jpeg, .png, .gif and .mp4 files. @@ -54,18 +62,17 @@ def upload_file(self, f): """ with FilesOpener(f) as files: response = self.session.post( - 'https://{}/upload'.format(self.domain), - files=files + "https://{}/upload".format(self.domain), files=files ).json() if isinstance(response, list): - error = response[0].get('error') + error = response[0].get("error") else: - error = response.get('error') + error = response.get("error") if error: - if isinstance(error, str) and error.startswith('FLOOD_WAIT_'): - retry_after = int(error.rsplit('_',1)[-1]) + if isinstance(error, str) and error.startswith("FLOOD_WAIT_"): + retry_after = int(error.rsplit("_", 1)[-1]) raise RetryAfterError(retry_after) else: raise TelegraphException(error) @@ -74,24 +81,29 @@ def upload_file(self, f): class Telegraph: - """ Telegraph API client helper + """Telegraph API client helper :param access_token: access token :param domain: domain (e.g. alternative mirror graph.org) """ - __slots__ = ('_telegraph',) + __slots__ = ("_telegraph",) - def __init__(self, access_token=None, domain='telegra.ph'): + def __init__(self, access_token: Optional[str] = None, domain: str = "telegra.ph"): self._telegraph = TelegraphApi(access_token, domain) def get_access_token(self): """Get current access_token""" return self._telegraph.access_token - def create_account(self, short_name, author_name=None, author_url=None, - replace_token=True): - """ Create a new Telegraph account + def create_account( + self, + short_name: str, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + replace_token: bool = True, + ): + """Create a new Telegraph account :param short_name: Account name, helps users with several accounts remember which they are currently using. @@ -103,20 +115,27 @@ def create_account(self, short_name, author_name=None, author_url=None, not necessarily to a Telegram profile or channels :param replace_token: Replaces current token to a new user's token """ - response = self._telegraph.method('createAccount', values={ - 'short_name': short_name, - 'author_name': author_name, - 'author_url': author_url - }) + response = self._telegraph.method( + "createAccount", + values={ + "short_name": short_name, + "author_name": author_name, + "author_url": author_url, + }, + ) if replace_token: - self._telegraph.access_token = response.get('access_token') + self._telegraph.access_token = response.get("access_token") return response - def edit_account_info(self, short_name=None, author_name=None, - author_url=None): - """ Update information about a Telegraph account. + def edit_account_info( + self, + short_name: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + ): + """Update information about a Telegraph account. Pass only the parameters that you want to edit :param short_name: Account name, helps users with several @@ -128,44 +147,56 @@ def edit_account_info(self, short_name=None, author_name=None, author's name below the title. Can be any link, not necessarily to a Telegram profile or channels """ - return self._telegraph.method('editAccountInfo', values={ - 'short_name': short_name, - 'author_name': author_name, - 'author_url': author_url - }) + return self._telegraph.method( + "editAccountInfo", + values={ + "short_name": short_name, + "author_name": author_name, + "author_url": author_url, + }, + ) def revoke_access_token(self): - """ Revoke access_token and generate a new one, for example, - if the user would like to reset all connected sessions, or - you have reasons to believe the token was compromised. - On success, returns dict with new access_token and auth_url fields + """Revoke access_token and generate a new one, for example, + if the user would like to reset all connected sessions, or + you have reasons to believe the token was compromised. + On success, returns dict with new access_token and auth_url fields """ - response = self._telegraph.method('revokeAccessToken') + response = self._telegraph.method("revokeAccessToken") - self._telegraph.access_token = response.get('access_token') + self._telegraph.access_token = response.get("access_token") return response - def get_page(self, path, return_content=True, return_html=True): - """ Get a Telegraph page + def get_page( + self, path: str, return_content: bool = True, return_html: bool = True + ): + """Get a Telegraph page :param path: Path to the Telegraph page (in the format Title-12-31, i.e. everything that comes after https://telegra.ph/) :param return_content: If true, content field will be returned :param return_html: If true, returns HTML instead of Nodes list """ - response = self._telegraph.method('getPage', path=path, values={ - 'return_content': return_content - }) + response = self._telegraph.method( + "getPage", path=path, values={"return_content": return_content} + ) if return_content and return_html: - response['content'] = nodes_to_html(response['content']) + response["content"] = nodes_to_html(response["content"]) return response - def create_page(self, title, content=None, html_content=None, - author_name=None, author_url=None, return_content=False): - """ Create a new Telegraph page + def create_page( + self, + title: str, + content: List[Any] = None, + html_content: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + return_content: bool = False, + ): + """Create a new Telegraph page :param title: Page title :param content: Content in nodes list format (see doc) @@ -180,17 +211,28 @@ def create_page(self, title, content=None, html_content=None, content_json = json_dumps(content) - return self._telegraph.method('createPage', values={ - 'title': title, - 'author_name': author_name, - 'author_url': author_url, - 'content': content_json, - 'return_content': return_content - }) - - def edit_page(self, path, title, content=None, html_content=None, - author_name=None, author_url=None, return_content=False): - """ Edit an existing Telegraph page + return self._telegraph.method( + "createPage", + values={ + "title": title, + "author_name": author_name, + "author_url": author_url, + "content": content_json, + "return_content": return_content, + }, + ) + + def edit_page( + self, + path: str, + title: str, + content: List[Any] = None, + html_content: Union[str, None] = None, + author_name: Union[str, None] = None, + author_url: Union[str, None] = None, + return_content: bool = False, + ): + """Edit an existing Telegraph page :param path: Path to the page :param title: Page title @@ -206,28 +248,32 @@ def edit_page(self, path, title, content=None, html_content=None, content_json = json_dumps(content) - return self._telegraph.method('editPage', path=path, values={ - 'title': title, - 'author_name': author_name, - 'author_url': author_url, - 'content': content_json, - 'return_content': return_content - }) - - def get_account_info(self, fields=None): - """ Get information about a Telegraph account + return self._telegraph.method( + "editPage", + path=path, + values={ + "title": title, + "author_name": author_name, + "author_url": author_url, + "content": content_json, + "return_content": return_content, + }, + ) + + def get_account_info(self, fields: Union[List[str], None] = None): + """Get information about a Telegraph account :param fields: List of account fields to return. Available fields: short_name, author_name, author_url, auth_url, page_count Default: [“short_name”,“author_name”,“author_url”] """ - return self._telegraph.method('getAccountInfo', { - 'fields': json_dumps(fields) if fields else None - }) + return self._telegraph.method( + "getAccountInfo", {"fields": json_dumps(fields) if fields else None} + ) - def get_page_list(self, offset=0, limit=50): - """ Get a list of pages belonging to a Telegraph account + def get_page_list(self, offset: int = 0, limit: int = 50): + """Get a list of pages belonging to a Telegraph account sorted by most recently created pages first :param offset: Sequential number of the first page to be returned @@ -235,13 +281,17 @@ def get_page_list(self, offset=0, limit=50): :param limit: Limits the number of pages to be retrieved (0-200, default = 50) """ - return self._telegraph.method('getPageList', { - 'offset': offset, - 'limit': limit - }) - - def get_views(self, path, year=None, month=None, day=None, hour=None): - """ Get the number of views for a Telegraph article + return self._telegraph.method("getPageList", {"offset": offset, "limit": limit}) + + def get_views( + self, + path: str, + year: Union[int, None] = None, + month: Union[int, None] = None, + day: Union[int, None] = None, + hour: Union[int, None] = None, + ): + """Get the number of views for a Telegraph article :param path: Path to the Telegraph page :param year: Required if month is passed. If passed, the number of @@ -253,15 +303,14 @@ def get_views(self, path, year=None, month=None, day=None, hour=None): :param hour: If passed, the number of page views for the requested hour will be returned """ - return self._telegraph.method('getViews', path=path, values={ - 'year': year, - 'month': month, - 'day': day, - 'hour': hour - }) - - def upload_file(self, f): - """ Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK + return self._telegraph.method( + "getViews", + path=path, + values={"year": year, "month": month, "day": day, "hour": hour}, + ) + + def upload_file(self, f: Union[PathLike, BinaryIO]): + """Upload file. NOT PART OF OFFICIAL API, USE AT YOUR OWN RISK Returns a list of dicts with `src` key. Allowed only .jpg, .jpeg, .png, .gif and .mp4 files. @@ -269,3 +318,13 @@ def upload_file(self, f): :type f: file, str or list """ return self._telegraph.upload_file(f) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + self.close() + + def close(self): + if not self._telegraph.session.is_closed: + self._telegraph.session.close() diff --git a/telegraph/exceptions.py b/telegraph/exceptions.py index 1bfaa60..6200b33 100644 --- a/telegraph/exceptions.py +++ b/telegraph/exceptions.py @@ -1,22 +1,34 @@ - - class TelegraphException(Exception): + """Base exception class for all Telegraph-related errors.""" + pass class ParsingException(Exception): + """Base exception class for all parsing-related errors.""" + pass class NotAllowedTag(ParsingException): + """Raised when an HTML tag is not supported by the parser.""" + pass class InvalidHTML(ParsingException): + """Raised when the provided HTML is malformed or invalid.""" + pass class RetryAfterError(TelegraphException): + """Raised when flood control is triggered and retry is required after a delay. + + Attributes: + retry_after (int): Time in seconds after which the request can be retried. + """ + def __init__(self, retry_after: int): self.retry_after = retry_after - super().__init__(f'Flood control exceeded. Retry in {retry_after} seconds') + super().__init__(f"Flood control exceeded. Retry in {retry_after} seconds") diff --git a/telegraph/upload.py b/telegraph/upload.py index 5711ada..71959cf 100644 --- a/telegraph/upload.py +++ b/telegraph/upload.py @@ -1,14 +1,14 @@ - from .api import TelegraphApi def upload_file(f): """Deprecated, use Telegraph.upload_file""" import warnings + warnings.warn( "telegraph.upload_file is deprecated, use Telegraph.upload_file", - DeprecationWarning + DeprecationWarning, ) r = TelegraphApi().upload_file(f) - return [i['src'] for i in r] + return [i["src"] for i in r] diff --git a/telegraph/utils.py b/telegraph/utils.py index 9960eae..fe8cf8e 100644 --- a/telegraph/utils.py +++ b/telegraph/utils.py @@ -1,7 +1,18 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import mimetypes import re import json + +from os import PathLike +from typing import Union, List + +try: + from typing import BinaryIO +except ImportError: + from typing.io import BinaryIO + from html.parser import HTMLParser from html.entities import name2codepoint from html import escape @@ -9,26 +20,92 @@ from .exceptions import NotAllowedTag, InvalidHTML -RE_WHITESPACE = re.compile(r'(\s+)', re.UNICODE) +RE_WHITESPACE = re.compile(r"(\s+)", re.UNICODE) ALLOWED_TAGS = { - 'a', 'aside', 'b', 'blockquote', 'br', 'code', 'em', 'figcaption', 'figure', - 'h3', 'h4', 'hr', 'i', 'iframe', 'img', 'li', 'ol', 'p', 'pre', 's', - 'strong', 'u', 'ul', 'video' + "a", + "aside", + "b", + "blockquote", + "br", + "code", + "em", + "figcaption", + "figure", + "h3", + "h4", + "hr", + "i", + "iframe", + "img", + "li", + "ol", + "p", + "pre", + "s", + "strong", + "u", + "ul", + "video", } VOID_ELEMENTS = { - 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', - 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr' + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "menuitem", + "meta", + "param", + "source", + "track", + "wbr", } BLOCK_ELEMENTS = { - 'address', 'article', 'aside', 'blockquote', 'canvas', 'dd', 'div', 'dl', - 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', - 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', 'nav', - 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tfoot', 'ul', - 'video' + "address", + "article", + "aside", + "blockquote", + "canvas", + "dd", + "div", + "dl", + "dt", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "hgroup", + "hr", + "li", + "main", + "nav", + "noscript", + "ol", + "output", + "p", + "pre", + "section", + "table", + "tfoot", + "ul", + "video", } @@ -49,11 +126,11 @@ def add_str_node(self, s): if not s: return - if 'pre' not in self.tags_path: # keep whitespace in
- s = RE_WHITESPACE.sub(' ', s)
+ if "pre" not in self.tags_path: # keep whitespace in
+ s = RE_WHITESPACE.sub(" ", s)
- if self.last_text_node is None or self.last_text_node.endswith(' '):
- s = s.lstrip(' ')
+ if self.last_text_node is None or self.last_text_node.endswith(" "):
+ s = s.lstrip(" ")
if not s:
self.last_text_node = None
@@ -68,44 +145,44 @@ def add_str_node(self, s):
def handle_starttag(self, tag, attrs_list):
if tag not in ALLOWED_TAGS:
- raise NotAllowedTag(f'{tag!r} tag is not allowed')
+ raise NotAllowedTag(f"{tag!r} tag is not allowed")
if tag in BLOCK_ELEMENTS:
self.last_text_node = None
- node = {'tag': tag}
+ node = {"tag": tag}
self.tags_path.append(tag)
self.current_nodes.append(node)
if attrs_list:
attrs = {}
- node['attrs'] = attrs
+ node["attrs"] = attrs
for attr, value in attrs_list:
attrs[attr] = value
if tag not in VOID_ELEMENTS:
self.parent_nodes.append(self.current_nodes)
- self.current_nodes = node['children'] = []
+ self.current_nodes = node["children"] = []
def handle_endtag(self, tag):
if tag in VOID_ELEMENTS:
return
if not len(self.parent_nodes):
- raise InvalidHTML(f'{tag!r} missing start tag')
+ raise InvalidHTML(f"{tag!r} missing start tag")
self.current_nodes = self.parent_nodes.pop()
last_node = self.current_nodes[-1]
- if last_node['tag'] != tag:
- raise InvalidHTML(f'{tag!r} tag closed instead of {last_node["tag"]!r}')
+ if last_node["tag"] != tag:
+ raise InvalidHTML(f"{tag!r} tag closed instead of {last_node['tag']!r}")
self.tags_path.pop()
- if not last_node['children']:
- last_node.pop('children')
+ if not last_node["children"]:
+ last_node.pop("children")
def handle_data(self, data):
self.add_str_node(data)
@@ -114,7 +191,7 @@ def handle_entityref(self, name):
self.add_str_node(chr(name2codepoint[name]))
def handle_charref(self, name):
- if name.startswith('x'):
+ if name.startswith("x"):
c = chr(int(name[1:], 16))
else:
c = chr(int(name))
@@ -123,8 +200,8 @@ def handle_charref(self, name):
def get_nodes(self):
if self.parent_nodes:
- not_closed_tag = self.parent_nodes[-1][-1]['tag']
- raise InvalidHTML(f'{not_closed_tag!r} tag is not closed')
+ not_closed_tag = self.parent_nodes[-1][-1]["tag"]
+ raise InvalidHTML(f"{not_closed_tag!r} tag is not closed")
return self.nodes
@@ -150,7 +227,7 @@ def nodes_to_html(nodes):
if not stack:
break
curr, i = stack.pop()
- append(f'{curr[i]["tag"]}>')
+ append(f"{curr[i]['tag']}>")
continue
node = curr[i]
@@ -159,28 +236,32 @@ def nodes_to_html(nodes):
append(escape(node))
continue
- append(f'<{node["tag"]}')
+ append(f"<{node['tag']}")
- if node.get('attrs'):
- for attr, value in node['attrs'].items():
+ if node.get("attrs"):
+ for attr, value in node["attrs"].items():
append(f' {attr}="{escape(value)}"')
- if node.get('children'):
- append('>')
+ if node.get("children"):
+ append(">")
stack.append((curr, i))
- curr, i = node['children'], -1
+ curr, i = node["children"], -1
continue
if node["tag"] in VOID_ELEMENTS:
- append('/>')
+ append("/>")
else:
- append(f'>{node["tag"]}>')
+ append(f">{node['tag']}>")
- return ''.join(out)
+ return "".join(out)
class FilesOpener(object):
- def __init__(self, paths, key_format='file{}'):
+ def __init__(
+ self,
+ paths: Union[PathLike[str], List[PathLike[str]], BinaryIO],
+ key_format: str = "file{}",
+ ):
if not isinstance(paths, list):
paths = [paths]
@@ -200,28 +281,26 @@ def open_files(self):
files = []
for x, file_or_name in enumerate(self.paths):
- name = ''
+ name = ""
if isinstance(file_or_name, tuple) and len(file_or_name) >= 2:
name = file_or_name[1]
file_or_name = file_or_name[0]
- if hasattr(file_or_name, 'read'):
+ if hasattr(file_or_name, "read"):
f = file_or_name
- if hasattr(f, 'name'):
+ if hasattr(f, "name"):
filename = f.name
else:
filename = name
else:
filename = file_or_name
- f = open(filename, 'rb')
+ f = open(filename, "rb")
self.opened_files.append(f)
mimetype = mimetypes.MimeTypes().guess_type(filename)[0]
- files.append(
- (self.key_format.format(x), ('file{}'.format(x), f, mimetype))
- )
+ files.append((self.key_format.format(x), ("file{}".format(x), f, mimetype)))
return files
@@ -233,4 +312,4 @@ def close_files(self):
def json_dumps(*args, **kwargs):
- return json.dumps(*args, **kwargs, separators=(',', ':'), ensure_ascii=False)
+ return json.dumps(*args, **kwargs, separators=(",", ":"), ensure_ascii=False)
diff --git a/tests/__init__.py b/tests/__init__.py
index 92a9210..e75350c 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,2 +1,4 @@
from . import test_html_converter
from . import test_telegraph
+
+__all__ = ["test_html_converter", "test_telegraph"]
diff --git a/tests/test_html_converter.py b/tests/test_html_converter.py
index da064d1..a340638 100644
--- a/tests/test_html_converter.py
+++ b/tests/test_html_converter.py
@@ -10,20 +10,27 @@
-""".replace('\n', '')
+""".replace("\n", "")
NODES_TEST_LIST = [
- {'tag': 'p', 'children': ['Hello, world!', {'tag': 'br'}]},
- {'tag': 'p', 'children': [{
- 'tag': 'a',
- 'attrs': {'href': 'https://telegra.ph/'},
- 'children': ['Test link']
- }]
- },
- {'tag': 'figure', 'children': [
- {'tag': 'img', 'attrs': {'src': '/file/6c2ecfdfd6881d37913fa.png'}},
- {'tag': 'figcaption'}
- ]}
+ {"tag": "p", "children": ["Hello, world!", {"tag": "br"}]},
+ {
+ "tag": "p",
+ "children": [
+ {
+ "tag": "a",
+ "attrs": {"href": "https://telegra.ph/"},
+ "children": ["Test link"],
+ }
+ ],
+ },
+ {
+ "tag": "figure",
+ "children": [
+ {"tag": "img", "attrs": {"src": "/file/6c2ecfdfd6881d37913fa.png"}},
+ {"tag": "figcaption"},
+ ],
+ },
]
HTML_MULTI_LINES = """
@@ -39,10 +46,7 @@
HTML_MULTI_LINES1 = """Hello, world!
"""
HTML_MULTI_LINES_NODES_LIST = [
- {'tag': 'p', 'children': [
- {'tag': 'b', 'children': ['Hello, ']},
- 'world! '
- ]},
+ {"tag": "p", "children": [{"tag": "b", "children": ["Hello, "]}, "world! "]},
]
HTML_NO_STARTTAG = ""
@@ -50,36 +54,23 @@
class TestHTMLConverter(TestCase):
def test_html_to_nodes(self):
- self.assertEqual(
- html_to_nodes(HTML_TEST_STR),
- NODES_TEST_LIST
- )
+ self.assertEqual(html_to_nodes(HTML_TEST_STR), NODES_TEST_LIST)
def test_nodes_to_html(self):
- self.assertEqual(
- nodes_to_html(NODES_TEST_LIST),
- HTML_TEST_STR
- )
+ self.assertEqual(nodes_to_html(NODES_TEST_LIST), HTML_TEST_STR)
def test_html_to_nodes_multi_line(self):
- self.assertEqual(
- html_to_nodes(HTML_MULTI_LINES),
- HTML_MULTI_LINES_NODES_LIST
- )
- self.assertEqual(
- html_to_nodes(HTML_MULTI_LINES1),
- HTML_MULTI_LINES_NODES_LIST
- )
+ self.assertEqual(html_to_nodes(HTML_MULTI_LINES), HTML_MULTI_LINES_NODES_LIST)
+ self.assertEqual(html_to_nodes(HTML_MULTI_LINES1), HTML_MULTI_LINES_NODES_LIST)
def test_uppercase_tags(self):
self.assertEqual(
- html_to_nodes("Hello
"),
- [{'tag': 'p', 'children': ['Hello']}]
+ html_to_nodes("Hello
"), [{"tag": "p", "children": ["Hello"]}]
)
def test_html_to_nodes_invalid_html(self):
with self.assertRaises(InvalidHTML):
- html_to_nodes('
')
+ html_to_nodes("
")
def test_html_to_nodes_not_allowed_tag(self):
with self.assertRaises(NotAllowedTag):
@@ -87,57 +78,66 @@ def test_html_to_nodes_not_allowed_tag(self):
def test_nodes_to_html_nested(self):
self.assertEqual(
- nodes_to_html([
- {'tag': 'a', 'children': [
- {'tag': 'b', 'children': [
- {'tag': 'c', 'children': [
- {'tag': 'd', 'children': []}
- ]}
- ]}
- ]}
- ]),
- ' '
+ nodes_to_html(
+ [
+ {
+ "tag": "a",
+ "children": [
+ {
+ "tag": "b",
+ "children": [
+ {
+ "tag": "c",
+ "children": [{"tag": "d", "children": []}],
+ }
+ ],
+ }
+ ],
+ }
+ ]
+ ),
+ " ",
)
def test_nodes_to_html_blank(self):
- self.assertEqual(
- nodes_to_html([]),
- ''
- )
+ self.assertEqual(nodes_to_html([]), "")
def test_clear_whitespace(self):
- i = (
- '\nA B C'
- ' D E
F
\n'
- )
+ i = "\nA B C D E
F
\n"
expected = [
- {'tag': 'p', 'children': [
- {'tag': 'i', 'children': ['A']},
- {'tag': 'b', 'children': [' ']},
- {'tag': 'b', 'children': [
- 'B ',
- {'tag': 'i', 'children': ['C']},
- {'tag': 'i', 'children': [{'tag': 'b'}]},
- ' D '
- ]},
- 'E '
- ]},
- {'tag': 'p', 'children': ['F ']}
+ {
+ "tag": "p",
+ "children": [
+ {"tag": "i", "children": ["A"]},
+ {"tag": "b", "children": [" "]},
+ {
+ "tag": "b",
+ "children": [
+ "B ",
+ {"tag": "i", "children": ["C"]},
+ {"tag": "i", "children": [{"tag": "b"}]},
+ " D ",
+ ],
+ },
+ "E ",
+ ],
+ },
+ {"tag": "p", "children": ["F "]},
]
self.assertEqual(html_to_nodes(i), expected)
def test_clear_whitespace_1(self):
- x = '\nA B C D E
F
\n'
- y = 'A B C D E
F
'
+ x = "\nA B C D E
F
\n"
+ y = "A B C D E
F
"
self.assertEqual(nodes_to_html(html_to_nodes(x)), y)
def test_pre_whitespace_preserved(self):
self.assertEqual(
html_to_nodes("\nhello\nworld
"),
- [{'tag': 'pre', 'children': ['\nhello\nworld']}]
+ [{"tag": "pre", "children": ["\nhello\nworld"]}],
)
def test_no_starttag_node(self):
with self.assertRaises(InvalidHTML):
- html_to_nodes(HTML_NO_STARTTAG)
+ html_to_nodes(HTML_NO_STARTTAG)
diff --git a/tests/test_telegraph.py b/tests/test_telegraph.py
index ad15cfb..a47ff31 100644
--- a/tests/test_telegraph.py
+++ b/tests/test_telegraph.py
@@ -5,68 +5,64 @@
POST_HTML_CONTENT = """
-""".replace('\n', '')
+""".replace("\n", "")
class TestTelegraph(TestCase):
- @skip('unstable page creation, PAGE_SAVE_FAILED')
+ @skip("unstable page creation, PAGE_SAVE_FAILED")
def test_flow(self):
telegraph = Telegraph()
response = telegraph.create_account(
- short_name='python telegraph',
- author_name='Python Telegraph API wrapper',
- author_url='https://github.com/python273/telegraph'
+ short_name="python telegraph",
+ author_name="Python Telegraph API wrapper",
+ author_url="https://github.com/python273/telegraph",
)
- self.assertTrue('access_token' in response)
- self.assertTrue('auth_url' in response)
+ self.assertTrue("access_token" in response)
+ self.assertTrue("auth_url" in response)
response = telegraph.get_account_info()
- self.assertEqual(response['short_name'], 'python telegraph')
+ self.assertEqual(response["short_name"], "python telegraph")
- response = telegraph.edit_account_info(
- short_name='Python Telegraph Wrapper'
- )
- self.assertEqual(response['short_name'], 'Python Telegraph Wrapper')
+ response = telegraph.edit_account_info(short_name="Python Telegraph Wrapper")
+ self.assertEqual(response["short_name"], "Python Telegraph Wrapper")
response = telegraph.create_page(
- 'Python Telegraph API wrapper',
- html_content='Hello, world!
'
+ "Python Telegraph API wrapper", html_content="Hello, world!
"
)
telegraph.edit_page(
- response['path'],
- 'Python Telegraph API Wrapper',
- html_content=POST_HTML_CONTENT
+ response["path"],
+ "Python Telegraph API Wrapper",
+ html_content=POST_HTML_CONTENT,
)
- response = telegraph.get_views(response['path'])
- self.assertTrue('views' in response)
+ response = telegraph.get_views(response["path"])
+ self.assertTrue("views" in response)
response = telegraph.get_page_list()
- self.assertTrue(response['total_count'] > 0)
+ self.assertTrue(response["total_count"] > 0)
response = telegraph.revoke_access_token()
- self.assertTrue('access_token' in response)
- self.assertTrue('auth_url' in response)
+ self.assertTrue("access_token" in response)
+ self.assertTrue("auth_url" in response)
def test_get_page_html(self):
telegraph = Telegraph()
- response = telegraph.get_page('Hey-01-17-2')
+ response = telegraph.get_page("Hey-01-17-2")
- self.assertEqual(response['content'], 'Hello, world!
')
+ self.assertEqual(response["content"], "Hello, world!
")
def test_get_page(self):
telegraph = Telegraph()
- response = telegraph.get_page('Hey-01-17-2', return_html=False)
+ response = telegraph.get_page("Hey-01-17-2", return_html=False)
self.assertEqual(
- response['content'],
- [{'tag': 'p', 'children': ['Hello, world!']}]
+ response["content"], [{"tag": "p", "children": ["Hello, world!"]}]
)
def test_get_page_without_content(self):
telegraph = Telegraph()
- response = telegraph.get_page('Hey-01-17-2', return_content=False)
+ response = telegraph.get_page("Hey-01-17-2", return_content=False)
- self.assertTrue('content' not in response)
+ self.assertTrue("content" not in response)