From afcb52769b536f46ee3d589a4a8c58f70bfce2a7 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 21:18:14 +0900 Subject: [PATCH 01/12] Used ruff --- docs/conf.py | 70 +++++------ generate_async_api.py | 12 +- setup.py | 52 ++++----- telegraph/__init__.py | 10 +- telegraph/aio.py | 219 ++++++++++++++++++++--------------- telegraph/api.py | 201 ++++++++++++++++++-------------- telegraph/exceptions.py | 4 +- telegraph/upload.py | 6 +- telegraph/utils.py | 156 +++++++++++++++++-------- tests/__init__.py | 2 + tests/test_html_converter.py | 138 +++++++++++----------- tests/test_telegraph.py | 54 ++++----- 12 files changed, 514 insertions(+), 410 deletions(-) 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/setup.py b/setup.py index ab9a5c2..b6a4dd1 100755 --- a/setup.py +++ b/setup.py @@ -14,46 +14,40 @@ 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'], + license="MIT", + packages=["telegraph"], + install_requires=["requests"], extras_require={ - 'aio': ['httpx'], + "aio": ["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", + ], ) 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..c1091ed 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import json import httpx @@ -8,7 +7,7 @@ class TelegraphApi: - """ Telegraph API Client + """Telegraph API Client :param access_token: access_token :type access_token: str @@ -16,36 +15,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=None, domain="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, values=None, path=""): 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 + """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 +53,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 +75,25 @@ 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=None, domain="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, author_name=None, author_url=None, replace_token=True + ): + """Create a new Telegraph account :param short_name: Account name, helps users with several accounts remember which they are currently using. @@ -103,20 +105,24 @@ 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=None, author_name=None, author_url=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 +134,54 @@ 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 + """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, + content=None, + html_content=None, + author_name=None, + author_url=None, + return_content=False, + ): + """Create a new Telegraph page :param title: Page title :param content: Content in nodes list format (see doc) @@ -180,17 +196,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, + title, + content=None, + html_content=None, + author_name=None, + author_url=None, + return_content=False, + ): + """Edit an existing Telegraph page :param path: Path to the page :param title: Page title @@ -206,28 +233,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 - })) + 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 + """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 + """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 +266,12 @@ 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 - })) + 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 + """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 +283,18 @@ 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 - })) + 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 + """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) diff --git a/telegraph/api.py b/telegraph/api.py index f4c7569..772a484 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import json import requests @@ -8,7 +7,7 @@ class TelegraphApi: - """ Telegraph API Client + """Telegraph API Client :param access_token: access_token :type access_token: str @@ -16,36 +15,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=None, domain="telegra.ph"): self.access_token = access_token self.domain = domain self.session = requests.Session() - def method(self, method, values=None, path=''): + def method(self, method, values=None, path=""): 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 + """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 +52,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 +71,25 @@ 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=None, domain="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, author_name=None, author_url=None, replace_token=True + ): + """Create a new Telegraph account :param short_name: Account name, helps users with several accounts remember which they are currently using. @@ -103,20 +101,22 @@ 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=None, author_name=None, author_url=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 +128,54 @@ 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 + """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, + content=None, + html_content=None, + author_name=None, + author_url=None, + return_content=False, + ): + """Create a new Telegraph page :param title: Page title :param content: Content in nodes list format (see doc) @@ -180,17 +190,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, + title, + content=None, + html_content=None, + author_name=None, + author_url=None, + return_content=False, + ): + """Edit an existing Telegraph page :param path: Path to the page :param title: Page title @@ -206,28 +227,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 - }) + 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 + """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 + """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 +260,10 @@ 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 - }) + 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 + """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 +275,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 - }) + 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 + """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. diff --git a/telegraph/exceptions.py b/telegraph/exceptions.py index 1bfaa60..3c3a70f 100644 --- a/telegraph/exceptions.py +++ b/telegraph/exceptions.py @@ -1,5 +1,3 @@ - - class TelegraphException(Exception): pass @@ -19,4 +17,4 @@ class InvalidHTML(ParsingException): class RetryAfterError(TelegraphException): 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..0921389 100644 --- a/telegraph/utils.py +++ b/telegraph/utils.py @@ -9,26 +9,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 +115,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 +134,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 +180,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 +189,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 +216,7 @@ def nodes_to_html(nodes):
             if not stack:
                 break
             curr, i = stack.pop()
-            append(f'')
+            append(f"")
             continue
 
         node = curr[i]
@@ -159,28 +225,28 @@ 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'>')
+            append(f">")
 
-    return ''.join(out)
+    return "".join(out)
 
 
 class FilesOpener(object):
-    def __init__(self, paths, key_format='file{}'):
+    def __init__(self, paths, key_format="file{}"):
         if not isinstance(paths, list):
             paths = [paths]
 
@@ -200,28 +266,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 +297,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 = ( - '\n

A B C' - ' D E

F

\n' - ) + i = "\n

A 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 = '\n

A B C D E

F

\n' - y = '

A B C D E

F

' + x = "\n

A 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 = """

Python Telegraph API Wrapper

-""".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) From fb6ae85cb105c4832f3ae6fc624e950218ee91ef Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 21:30:43 +0900 Subject: [PATCH 02/12] deleted requests and changed to httpx --- requirements.txt | 5 ++--- telegraph/api.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index b3f0581..6088a08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -requests -httpx -libcst \ No newline at end of file +httpx==0.28.1 +libcst==1.8.6 \ No newline at end of file diff --git a/telegraph/api.py b/telegraph/api.py index 772a484..c3425ce 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -import requests +import httpx from .exceptions import TelegraphException, RetryAfterError from .utils import html_to_nodes, nodes_to_html, FilesOpener, json_dumps @@ -20,7 +20,7 @@ class TelegraphApi: def __init__(self, access_token=None, domain="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=""): values = values.copy() if values is not None else {} From 8f1ca1f3f2808056a49001d84a15883306df8683 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 22:52:26 +0900 Subject: [PATCH 03/12] added best typing --- telegraph/aio.py | 72 +++++++++++++++++++++++++++++--------------- telegraph/api.py | 74 +++++++++++++++++++++++++++++++--------------- telegraph/utils.py | 15 +++++++++- 3 files changed, 112 insertions(+), 49 deletions(-) diff --git a/telegraph/aio.py b/telegraph/aio.py index c1091ed..ef14ad2 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -2,6 +2,14 @@ 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 @@ -17,12 +25,12 @@ class TelegraphApi: __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: @@ -44,7 +52,7 @@ async def method(self, method, values=None, path=""): else: raise TelegraphException(error) - async def upload_file(self, f): + 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. @@ -83,7 +91,7 @@ class 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): @@ -91,7 +99,11 @@ def get_access_token(self): return self._telegraph.access_token async def create_account( - self, short_name, author_name=None, author_url=None, replace_token=True + 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 @@ -120,7 +132,10 @@ async def create_account( return response async def edit_account_info( - self, short_name=None, author_name=None, author_url=None + 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 @@ -155,7 +170,9 @@ async def revoke_access_token(self): return response - async def get_page(self, path, return_content=True, return_html=True): + 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, @@ -174,12 +191,12 @@ async def get_page(self, path, return_content=True, return_html=True): async def create_page( self, - title, - content=None, - html_content=None, - author_name=None, - author_url=None, - return_content=False, + 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 @@ -209,13 +226,13 @@ async def create_page( async def edit_page( self, - path, - title, - content=None, - html_content=None, - author_name=None, - author_url=None, - return_content=False, + 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 @@ -245,7 +262,7 @@ async def edit_page( }, ) - async def get_account_info(self, fields=None): + 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: @@ -257,7 +274,7 @@ async def get_account_info(self, fields=None): "getAccountInfo", {"fields": json_dumps(fields) if fields else None} ) - async def get_page_list(self, offset=0, limit=50): + 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 @@ -270,7 +287,14 @@ async def get_page_list(self, offset=0, limit=50): "getPageList", {"offset": offset, "limit": limit} ) - async def get_views(self, path, year=None, month=None, day=None, hour=None): + 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 @@ -289,7 +313,7 @@ async def get_views(self, path, year=None, month=None, day=None, hour=None): values={"year": year, "month": month, "day": day, "hour": hour}, ) - async def upload_file(self, f): + 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. diff --git a/telegraph/api.py b/telegraph/api.py index c3425ce..26589b1 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -2,6 +2,14 @@ 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 @@ -17,12 +25,12 @@ class TelegraphApi: __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.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: @@ -42,7 +50,7 @@ def method(self, method, values=None, path=""): else: raise TelegraphException(error) - def upload_file(self, f): + 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. @@ -79,7 +87,7 @@ class 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): @@ -87,7 +95,11 @@ def get_access_token(self): return self._telegraph.access_token def create_account( - self, short_name, author_name=None, author_url=None, replace_token=True + 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 @@ -115,7 +127,12 @@ def create_account( return response - def edit_account_info(self, short_name=None, author_name=None, author_url=None): + 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 @@ -149,7 +166,9 @@ def revoke_access_token(self): return response - def get_page(self, path, return_content=True, return_html=True): + 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, @@ -168,12 +187,12 @@ def get_page(self, path, return_content=True, return_html=True): def create_page( self, - title, - content=None, - html_content=None, - author_name=None, - author_url=None, - return_content=False, + 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 @@ -203,13 +222,13 @@ def create_page( def edit_page( self, - path, - title, - content=None, - html_content=None, - author_name=None, - author_url=None, - return_content=False, + 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 @@ -239,7 +258,7 @@ def edit_page( }, ) - def get_account_info(self, fields=None): + 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: @@ -251,7 +270,7 @@ def get_account_info(self, fields=None): "getAccountInfo", {"fields": json_dumps(fields) if fields else None} ) - def get_page_list(self, offset=0, limit=50): + 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 @@ -262,7 +281,14 @@ def get_page_list(self, offset=0, limit=50): """ return self._telegraph.method("getPageList", {"offset": offset, "limit": limit}) - def get_views(self, path, year=None, month=None, day=None, hour=None): + 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 @@ -281,7 +307,7 @@ def get_views(self, path, year=None, month=None, day=None, hour=None): values={"year": year, "month": month, "day": day, "hour": hour}, ) - def upload_file(self, f): + 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. diff --git a/telegraph/utils.py b/telegraph/utils.py index 0921389..b126fcc 100644 --- a/telegraph/utils.py +++ b/telegraph/utils.py @@ -2,6 +2,15 @@ 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 @@ -246,7 +255,11 @@ def nodes_to_html(nodes): 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] From db7a29d1249a983f206620b7d92fae4763b5f13d Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 22:58:53 +0900 Subject: [PATCH 04/12] exception dockstring error --- telegraph/exceptions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/telegraph/exceptions.py b/telegraph/exceptions.py index 3c3a70f..6200b33 100644 --- a/telegraph/exceptions.py +++ b/telegraph/exceptions.py @@ -1,20 +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") From 4fd9a3d78fc1286fd083453c7500e61a239be37d Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:07:30 +0900 Subject: [PATCH 05/12] Ci/Cd --- .github/workflows/python-publish.yml | 68 ++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 1b6745c..4ac78b6 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,26 +1,78 @@ -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-latest + 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 . + + - 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 From b2aa6c7dd8a2100737407e08f7b9938cdba827b9 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:11:41 +0900 Subject: [PATCH 06/12] runs-on changed --- .github/workflows/python-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 4ac78b6..0a3e217 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -11,7 +11,7 @@ on: jobs: test: name: Test on Python ${{ matrix.python-version }} - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] From d9c0481d9bc5ce7e81e42a9f7d9fd0fae5ac06ec Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:12:53 +0900 Subject: [PATCH 07/12] updated dependencies --- .github/workflows/python-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 0a3e217..2f99f1e 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -35,6 +35,7 @@ jobs: pip install pytest pytest-cov # Устанавливаем ваш пакет в режиме разработки pip install -e . + pip install -r requirements.txt - name: Run tests with pytest run: | From 619a4985be199f622e63d0906d1467b06b27925b Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:14:06 +0900 Subject: [PATCH 08/12] requirements.txt update --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6088a08..2f23525 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ httpx==0.28.1 -libcst==1.8.6 \ No newline at end of file +libcst \ No newline at end of file From 0fef11ec1471efc4321f8eb32a5dc08e05b1e239 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:16:53 +0900 Subject: [PATCH 09/12] added annotations --- telegraph/aio.py | 1 + telegraph/api.py | 1 + telegraph/utils.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/telegraph/aio.py b/telegraph/aio.py index ef14ad2..4da2b35 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import annotations import httpx diff --git a/telegraph/api.py b/telegraph/api.py index 26589b1..4e57ca0 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import annotations import httpx diff --git a/telegraph/utils.py b/telegraph/utils.py index b126fcc..fe8cf8e 100644 --- a/telegraph/utils.py +++ b/telegraph/utils.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import annotations + import mimetypes import re import json From e2cfbe9e56ac86d6e0cea24fa7d8a75a170fc950 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:18:02 +0900 Subject: [PATCH 10/12] requirements.txt againg --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2f23525..27a1c34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -httpx==0.28.1 +httpx libcst \ No newline at end of file From cec96d4327fe8580b2bd0a9b5488ca430a070a04 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:35:34 +0900 Subject: [PATCH 11/12] update setup and readme, and added context-manager supported --- README.md | 40 ++++++++++++++++++++++++++++++++++++---- setup.py | 7 +++---- telegraph/aio.py | 10 ++++++++++ telegraph/api.py | 10 ++++++++++ 4 files changed, 59 insertions(+), 8 deletions(-) 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/setup.py b/setup.py index b6a4dd1..a01cccf 100755 --- a/setup.py +++ b/setup.py @@ -35,10 +35,7 @@ ), license="MIT", packages=["telegraph"], - install_requires=["requests"], - extras_require={ - "aio": ["httpx"], - }, + install_requires=["httpx"], classifiers=[ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", @@ -49,5 +46,7 @@ "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/aio.py b/telegraph/aio.py index 4da2b35..e2d6f84 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -323,3 +323,13 @@ async def upload_file(self, f: Union[PathLike, BinaryIO]): :type f: file, str or list """ 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 4e57ca0..b053135 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -317,3 +317,13 @@ def upload_file(self, f: Union[PathLike, BinaryIO]): :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() From 5476830566e73eb0032e5e1a6122b60384224ef2 Mon Sep 17 00:00:00 2001 From: GameHipe Date: Sat, 28 Feb 2026 23:39:13 +0900 Subject: [PATCH 12/12] added margins --- telegraph/aio.py | 1 + telegraph/api.py | 1 + 2 files changed, 2 insertions(+) diff --git a/telegraph/aio.py b/telegraph/aio.py index e2d6f84..dc27837 100644 --- a/telegraph/aio.py +++ b/telegraph/aio.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + from __future__ import annotations import httpx diff --git a/telegraph/api.py b/telegraph/api.py index b053135..683bea6 100644 --- a/telegraph/api.py +++ b/telegraph/api.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + from __future__ import annotations import httpx