diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ea2a297..9d7037f 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -28,7 +28,6 @@ jobs: - name: Build release distributions run: | - # NOTE: put your own distribution build steps here. python -m pip install build python -m build @@ -67,4 +66,4 @@ jobs: - name: Publish release distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - packages-dir: dist/ \ No newline at end of file + packages-dir: dist/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c6592b8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,56 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + unittest: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt -e . + + - name: Run tests + run: | + python -m unittest discover -s tests + python -m compileall ytelegraph tests + + uv: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install uv + run: python -m pip install uv + + - name: Run tests with uv + run: | + uv --version + uv run --with-requirements requirements.txt python -m unittest discover -s tests + uv run --with-requirements requirements.txt python -m compileall ytelegraph tests diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bccb1c8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +YTelegraph is a small Python package for publishing Markdown and DOM content through the Telegraph API. + +- `ytelegraph/` contains the package code: `api.py` for page operations, `account.py` for token/account handling, and `md_to_dom.py` for Markdown conversion. +- `examples/` contains runnable usage examples for creating, editing, and inspecting Telegraph pages. +- `draft_tests/` contains exploratory/manual integration scripts. Many call the live Telegraph API or wait for user input. +- `setup.py`, `requirements.txt`, `README.md`, and `CHANGELOG.md` define packaging, dependencies, usage docs, and release history. + +## Build, Test, and Development Commands + +This project historically uses plain pip/setuptools. Use uv as an additional compatibility path, not as the source of truth. + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt -e . +``` + +Install the package in editable mode with runtime dependencies. + +```bash +python -m unittest discover -s tests +python -m compileall ytelegraph tests +python -m pip install build +python -m build +``` + +Use `compileall` as a quick syntax check. Use `python -m build` to verify source and wheel distributions; install `build` first if needed. + +Run the uv compatibility path intentionally: + +```bash +uv run --with-requirements requirements.txt python -m unittest discover -s tests +uv run --with-requirements requirements.txt python -m compileall ytelegraph tests +uv run --with build --with-requirements requirements.txt python -m build +``` + +```bash +python examples/basic_usage.py +python examples/second_usage.py +python draft_tests/test_space_missing.py +``` + +Run examples and draft tests intentionally because they may create or edit live Telegraph pages. + +## Coding Style & Naming Conventions + +Use Python 3.8-compatible syntax, 4-space indentation, and clear type hints for public interfaces. Keep module names lowercase with underscores. Follow existing API naming: Markdown helpers end in `_md`, append helpers use explicit names such as `edit_page_md_append_to_front`, and internal helpers are prefixed with `_`. + +Keep docstrings concise and focused on behavior, arguments, returns, and exceptions. Avoid broad refactors when fixing targeted API behavior. + +## Testing Guidelines + +The `tests/` directory contains deterministic unit tests. Treat `draft_tests/` as integration/manual coverage and add isolated tests when changing parsing or path-handling logic. New tests should be named `test_.py`; prefer deterministic tests for `md_to_dom` and API payload behavior that do not require network access. For live API checks, use disposable pages and avoid relying on existing public page paths. + +## Commit & Pull Request Guidelines + +Git history uses short messages, often with Conventional Commit prefixes such as `feat:`, `fix:`, and `chore:`. Prefer that style: `fix: preserve spaces in markdown formatting`. + +Pull requests should include a short problem summary, the implementation approach, commands run, and any live Telegraph API effects. Link related issues when available. Include screenshots or page URLs only when they help verify user-visible publishing behavior. + +## Security & Configuration Tips + +Never commit Telegraph access tokens or generated `ph_token.txt` files. Use `TELEGRA_PH_TOKEN` for an access token, and `PH_TOKEN_PATH` or a local ignored token file for manual testing. diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4095f..99f4c3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added deterministic unit tests for Markdown conversion, path handling, payload construction, token loading, and page retrieval behavior. +- Added CI coverage for pip and uv workflows across current Python versions. +- Added automatic `TELEGRA_PH_TOKEN` support. + +### Changed + +- Updated packaging classifiers through Python 3.14. +- Preserved Telegraph page response fields from `get_page`. + +### Fixed + +- Fixed `delete_page` verification. +- Added request timeouts for Telegraph API calls. +- Stopped printing newly created access tokens in account creation output and examples. + ## [0.2.1] - 2025-03-22 ### Fixed @@ -63,4 +82,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -- N/A \ No newline at end of file +- N/A diff --git a/README.md b/README.md index 5c7a016..0ee46a2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![PyPi Package Version](https://img.shields.io/pypi/v/your-telegraph.svg)](https://pypi.python.org/pypi/your-telegraph) [![Supported Python versions](https://img.shields.io/pypi/pyversions/your-telegraph.svg)](https://pypi.python.org/pypi/your-telegraph) [![PyPi downloads](https://img.shields.io/pypi/dm/your-telegraph.svg)](https://pypi.org/project/your-telegraph/) +[![PyPI Downloads](https://static.pepy.tech/personalized-badge/your-telegraph?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/your-telegraph) [![PyPi status](https://img.shields.io/pypi/status/your-telegraph.svg?style=flat-square)](https://pypi.python.org/pypi/your-telegraph) ![License](https://img.shields.io/github/license/alterxyz/ytelegraph) @@ -22,7 +23,7 @@ YTelegraph is a simple, user-friendly Python wrapper for the Telegraph API. Publ - [Advanced Usage](#advanced-usage) - [Token Management](#token-management) - [Create Account](#create-account) - - [Testing](#testing) + - [Development and Testing](#development-and-testing) - [Versioning](#versioning) - [Support](#support) - [Contributing](#contributing) @@ -93,7 +94,7 @@ This method is useful if you want to use an existing Telegraph account or manage ### Advanced Usage -Try and see the `example/second_usage.py` at [here](examples/second_usage.py). +Try and see the `examples/second_usage.py` at [here](examples/second_usage.py). ## Token Management @@ -102,6 +103,7 @@ YTelegraph offers flexible token management: 1. **Automatic**: If no token is provided, YTelegraph creates a new account and manages the token for you. 2. **Environment Variable**: Set the `TELEGRA_PH_TOKEN` environment variable, and YTelegraph will use it automatically. 3. **Direct Input**: Pass your token directly to the `TelegraphAPI` constructor. +4. **Token file path**: Set `PH_TOKEN_PATH` if you want the automatically created token stored somewhere other than the default token file. Choose the method that best fits your workflow and security requirements. @@ -137,9 +139,29 @@ For more details, check out the [Telegraph API documentation](https://telegra.ph YTelegraph makes this process super easy, but it's good to know how to do it manually if you ever need to. -## Testing +## Development and Testing -To run the basic integration tests, execute the examples in the `examples/` directory: +This project still uses the small setuptools/pip workflow it started with: + +```bash +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements.txt -e . +python -m unittest discover -s tests +python -m compileall ytelegraph tests +python -m pip install build +python -m build +``` + +You can also test the package with uv without converting the project to uv: + +```bash +uv run --with-requirements requirements.txt python -m unittest discover -s tests +uv run --with-requirements requirements.txt python -m compileall ytelegraph tests +uv run --with build --with-requirements requirements.txt python -m build +``` + +The examples in `examples/` and scripts in `draft_tests/` are live Telegraph API checks. Run them intentionally because they can create or edit Telegraph pages: ```bash python examples/basic_usage.py diff --git a/examples/second_usage.py b/examples/second_usage.py index f434b36..cfb3f48 100644 --- a/examples/second_usage.py +++ b/examples/second_usage.py @@ -14,11 +14,12 @@ my_token_file_path = my_ph._get_token_file_path() my_token = my_ph._get_token() +masked_token = f"{my_token[:4]}...{my_token[-4:]}" if my_token else None my_info = my_ph.get_account_info() print( - f"\nYour token file path: {my_token_file_path}\nYour token: {my_token}\nYour account info: {my_info}" + f"\nYour token file path: {my_token_file_path}\nYour token: {masked_token}\nYour account info: {my_info}" ) # login to your account by open the link in your browser diff --git a/setup.py b/setup.py index d49f980..a7db91b 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,9 @@ url="https://github.com/alterxyz/ytelegraph", packages=find_packages(), install_requires=[ - "requests", - "beautifulsoup4", - "Markdown", + "requests>=2.32.3,<3.0.0", + "beautifulsoup4>=4.12.3,<5.0.0", + "Markdown>=3.6,<4.0.0", ], classifiers=[ "Development Status :: 3 - Alpha", @@ -28,6 +28,8 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tests/test_account.py b/tests/test_account.py new file mode 100644 index 0000000..a8c604f --- /dev/null +++ b/tests/test_account.py @@ -0,0 +1,64 @@ +import json +import os +import tempfile +import unittest +from unittest.mock import patch + +from ytelegraph.account import TelegraphAccount + + +class FakeResponse: + def __init__(self, payload): + self.payload = payload + + def raise_for_status(self) -> None: + pass + + def json(self): + return self.payload + + +class TelegraphAccountTests(unittest.TestCase): + def test_prefers_environment_token(self) -> None: + with patch.dict(os.environ, {"TELEGRA_PH_TOKEN": "env-token"}): + with patch.object(TelegraphAccount, "get_account_info", return_value={}): + with patch.object(TelegraphAccount, "_get_token") as get_token: + with patch.object(TelegraphAccount, "_create_account") as create_account: + account = TelegraphAccount() + + self.assertEqual(account.access_token, "env-token") + get_token.assert_not_called() + create_account.assert_not_called() + + def test_get_account_info_serializes_fields_and_uses_timeout(self) -> None: + account = TelegraphAccount.__new__(TelegraphAccount) + account.base_url = "https://api.telegra.ph" + account.access_token = "token" + account.request_timeout = 3.0 + + with patch("ytelegraph.account.requests.get") as mock_get: + mock_get.return_value = FakeResponse( + {"ok": True, "result": {"short_name": "Sandbox", "page_count": 1}} + ) + + result = account.get_account_info(["short_name", "page_count"]) + + self.assertEqual(result["page_count"], 1) + _, kwargs = mock_get.call_args + self.assertEqual(kwargs["timeout"], 3.0) + self.assertEqual(json.loads(kwargs["params"]["fields"]), ["short_name", "page_count"]) + + def test_save_token_uses_configured_path(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + token_path = os.path.join(temp_dir, "token.txt") + account = TelegraphAccount.__new__(TelegraphAccount) + + with patch.dict(os.environ, {"PH_TOKEN_PATH": token_path}): + account._save_token("secret-token") + + with open(token_path, "r", encoding="utf-8") as token_file: + self.assertEqual(token_file.read(), "secret-token") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..cb3f216 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,99 @@ +import json +import unittest + +from ytelegraph.api import TelegraphAPI + + +class FakeAccount: + access_token = "token" + author_name = "Anonymous" + author_url = None + + +def make_api() -> TelegraphAPI: + api = TelegraphAPI.__new__(TelegraphAPI) + api.account = FakeAccount() + api.base_url = "https://api.telegra.ph" + api.request_timeout = 10.0 + return api + + +class TelegraphAPITests(unittest.TestCase): + def test_extract_path_accepts_paths_and_telegraph_urls(self) -> None: + api = make_api() + + self.assertEqual(api._extract_path("Sample-Page-12-15"), "Sample-Page-12-15") + self.assertEqual( + api._extract_path("https://telegra.ph/Sample-Page-12-15/"), + "Sample-Page-12-15", + ) + + def test_extract_path_rejects_unknown_urls(self) -> None: + api = make_api() + + with self.assertRaises(ValueError): + api._extract_path("https://example.com/Sample-Page-12-15") + + def test_create_page_sends_json_content(self) -> None: + api = make_api() + calls = [] + + def fake_request(method, endpoint, data): + calls.append((method, endpoint, data)) + return {"url": "https://telegra.ph/Sample-Page-12-15"} + + api._make_request = fake_request + url = api.create_page("Title", [{"tag": "p", "children": ["Hello"]}]) + + self.assertEqual(url, "https://telegra.ph/Sample-Page-12-15") + method, endpoint, data = calls[0] + self.assertEqual((method, endpoint), ("POST", "createPage")) + self.assertEqual(json.loads(data["content"]), [{"tag": "p", "children": ["Hello"]}]) + self.assertEqual(data["access_token"], "token") + + def test_get_page_preserves_page_fields(self) -> None: + api = make_api() + + def fake_request(method, endpoint, data): + self.assertEqual((method, endpoint), ("GET", "getPage")) + return { + "path": "Sample-Page-12-15", + "url": "https://telegra.ph/Sample-Page-12-15", + "title": "Title", + "description": "Description", + "views": 7, + "content": [{"tag": "p", "children": ["Hello"]}], + } + + api._make_request = fake_request + + page = api.get_page("Sample-Page-12-15") + + self.assertTrue(page["success"]) + self.assertEqual(page["path"], "Sample-Page-12-15") + self.assertEqual(page["views"], 7) + self.assertEqual(page["attempts"], 1) + self.assertIsNone(page["error"]) + + def test_delete_page_verifies_replaced_content(self) -> None: + api = make_api() + expected_content = [{"tag": "p", "children": ["This page has been deleted."]}] + calls = [] + + def fake_request(method, endpoint, data): + calls.append((method, endpoint, data)) + return {"url": "https://telegra.ph/Sample-Page-12-15"} + + api._make_request = fake_request + api.get_page = lambda path: { + "success": True, + "title": "404", + "content": expected_content, + } + + self.assertTrue(api.delete_page("Sample-Page-12-15")) + self.assertEqual(calls[0][1], "editPage") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_md_to_dom.py b/tests/test_md_to_dom.py new file mode 100644 index 0000000..a29f4e6 --- /dev/null +++ b/tests/test_md_to_dom.py @@ -0,0 +1,52 @@ +import unittest + +from ytelegraph import md_to_dom + + +class MarkdownToDomTests(unittest.TestCase): + def test_preserves_spaces_around_inline_formatting(self) -> None: + dom = md_to_dom("This is **bold** text and `code`.") + + self.assertEqual( + dom, + [ + { + "tag": "p", + "children": [ + "This is ", + {"tag": "strong", "children": ["bold"]}, + " text and ", + {"tag": "code", "children": ["code"]}, + ".", + ], + } + ], + ) + + def test_converts_headings_links_and_lists(self) -> None: + dom = md_to_dom( + "# Title\n\n" + "- **Bold text:** Use `**text**` to make text **bold**.\n\n" + "[Docs](https://telegra.ph/api)" + ) + + self.assertEqual(dom[0], {"tag": "h3", "children": ["Title"]}) + self.assertEqual(dom[1]["tag"], "ul") + self.assertEqual(dom[1]["children"][0]["tag"], "li") + self.assertEqual( + dom[2], + { + "tag": "p", + "children": [ + { + "tag": "a", + "attrs": {"href": "https://telegra.ph/api"}, + "children": ["Docs"], + } + ], + }, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ytelegraph/account.py b/ytelegraph/account.py index b1a7c23..b8f9bc1 100644 --- a/ytelegraph/account.py +++ b/ytelegraph/account.py @@ -39,6 +39,7 @@ def __init__( short_name: str = "Your Name", author_name: str = "Anonymous", author_url: Optional[str] = None, + request_timeout: float = 10.0, ) -> None: """Initializes a TelegraphAccount object. @@ -52,10 +53,13 @@ def __init__( Only used when creating a new account. Defaults to "Anonymous". author_url: The author URL associated with the account. Only used when creating a new account. Defaults to None. + request_timeout: Timeout in seconds for Telegraph API requests. """ self.base_url: str = "https://api.telegra.ph" + self.request_timeout: float = request_timeout self.access_token: str = ( access_token + or os.environ.get("TELEGRA_PH_TOKEN") or self._get_token() or self._create_account(short_name, author_name, author_url) ) @@ -95,7 +99,7 @@ def _get_token(self) -> Optional[str]: """ token_file: Path = self._get_token_file_path() if token_file.exists(): - with open(token_file, "r") as f: + with open(token_file, "r", encoding="utf-8") as f: return f.read().strip() return None @@ -107,8 +111,12 @@ def _save_token(self, token: str) -> None: """ token_file: Path = self._get_token_file_path() token_file.parent.mkdir(parents=True, exist_ok=True) - with open(token_file, "w") as f: + with open(token_file, "w", encoding="utf-8") as f: f.write(token) + try: + os.chmod(token_file, 0o600) + except OSError: + pass def _delete_token(self) -> None: """Deletes the access token file.""" @@ -137,11 +145,17 @@ def _create_account( "author_url": author_url, } try: - response: requests.Response = requests.post(url, data=data) + response: requests.Response = requests.post( + url, data=data, timeout=self.request_timeout + ) response.raise_for_status() - access_token: str = response.json()["result"]["access_token"] + result: Dict[str, Any] = response.json() + if not result.get("ok"): + print(f"Error creating account: {result.get('error', 'Unknown error')}") + return None + access_token: str = result["result"]["access_token"] self._save_token(access_token) - print(f"Account created with access token: {access_token}") + print("Account created and access token saved.") return access_token except RequestException as e: print(f"Error creating account: {e}") @@ -161,11 +175,19 @@ def get_account_info(self, fields: Optional[List[str]] = None) -> Dict[str, Any] url: str = f"{self.base_url}/getAccountInfo" params: Dict[str, Any] = {"access_token": self.access_token} if fields: - params["fields"] = "[" + ",".join(f'"{field}"' for field in fields) + "]" + params["fields"] = json.dumps(fields) try: - response: requests.Response = requests.get(url, params=params) + response: requests.Response = requests.get( + url, params=params, timeout=self.request_timeout + ) response.raise_for_status() - return response.json()["result"] + result: Dict[str, Any] = response.json() + if not result.get("ok"): + print( + f"Error getting account info: {result.get('error', 'Unknown error')}" + ) + return {} + return result["result"] except RequestException as e: print(f"Error getting account info: {e}") return {} @@ -191,10 +213,18 @@ def revoke_access_token(self) -> bool: url: str = f"{self.base_url}/revokeAccessToken" data: Dict[str, str] = {"access_token": self.access_token} try: - response: requests.Response = requests.post(url, data=data) + response: requests.Response = requests.post( + url, data=data, timeout=self.request_timeout + ) response.raise_for_status() + result: Dict[str, Any] = response.json() + if not result.get("ok"): + print( + f"Error revoking access token: {result.get('error', 'Unknown error')}" + ) + return False self._delete_token() - new_token: str = response.json()["result"]["access_token"] + new_token: str = result["result"]["access_token"] self._save_token(new_token) self.access_token = new_token return True @@ -221,16 +251,24 @@ def edit_account_info( """ url: str = f"{self.base_url}/editAccountInfo" data: Dict[str, Any] = {"access_token": self.access_token} - if short_name: + if short_name is not None: data["short_name"] = short_name - if author_name: + if author_name is not None: data["author_name"] = author_name - if author_url: + if author_url is not None: data["author_url"] = author_url try: - response: requests.Response = requests.post(url, data=data) + response: requests.Response = requests.post( + url, data=data, timeout=self.request_timeout + ) response.raise_for_status() - updated_info: Dict[str, Any] = response.json()["result"] + result: Dict[str, Any] = response.json() + if not result.get("ok"): + print( + f"Error editing account info: {result.get('error', 'Unknown error')}" + ) + return False + updated_info: Dict[str, Any] = result["result"] self.short_name = updated_info.get("short_name", self.short_name) self.author_name = updated_info.get("author_name", self.author_name) self.author_url = updated_info.get("author_url", self.author_url) diff --git a/ytelegraph/api.py b/ytelegraph/api.py index 114d394..f596caa 100644 --- a/ytelegraph/api.py +++ b/ytelegraph/api.py @@ -1,7 +1,7 @@ import json -import re import time -from typing import Optional, Dict, List, Any, Union +from typing import Optional, Dict, List, Any +from urllib.parse import urlparse import requests from requests.exceptions import RequestException @@ -36,6 +36,7 @@ def __init__( short_name: str = "Your Name", author_name: str = "Anonymous", author_url: Optional[str] = None, + request_timeout: float = 10.0, ) -> None: """Initializes a TelegraphAPI object. @@ -44,11 +45,13 @@ def __init__( short_name: The short name of the Telegraph account. author_name: The author name associated with the account. author_url: The author URL associated with the account. + request_timeout: Timeout in seconds for Telegraph API requests. """ self.account: TelegraphAccount = TelegraphAccount( - access_token, short_name, author_name, author_url + access_token, short_name, author_name, author_url, request_timeout ) self.base_url: str = "https://api.telegra.ph" + self.request_timeout: float = request_timeout def _make_request( self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None @@ -70,9 +73,13 @@ def _make_request( url: str = f"{self.base_url}/{endpoint}" try: if method.upper() == "GET": - response: requests.Response = requests.get(url, params=data) + response: requests.Response = requests.get( + url, params=data, timeout=self.request_timeout + ) else: - response: requests.Response = requests.post(url, data=data) + response: requests.Response = requests.post( + url, data=data, timeout=self.request_timeout + ) response.raise_for_status() result = response.json() if not result.get("ok"): @@ -94,12 +101,21 @@ def _extract_path(self, path_or_url: str) -> str: Raises: ValueError: If the input is not a valid Telegraph path or URL. """ - match = re.search( - r"(?:https?://(?:telegra\.ph/|telegraph\.com/))?([^/]+)/?$", path_or_url - ) - if match: - return match.group(1) - raise ValueError("Invalid path or URL format") + value = path_or_url.strip() + parsed = urlparse(value) + + if parsed.scheme or parsed.netloc: + host = parsed.netloc.lower() + allowed_hosts = {"telegra.ph", "www.telegra.ph", "telegraph.com"} + if parsed.scheme not in {"http", "https"} or host not in allowed_hosts: + raise ValueError("Invalid path or URL format") + path = parsed.path.strip("/") + else: + path = value.strip("/") + + if not path or "/" in path: + raise ValueError("Invalid path or URL format") + return path def create_page( self, @@ -312,11 +328,15 @@ def delete_page(self, path: str) -> bool: "author_name": "Deleted", "author_url": None, } - result: Dict[str, Any] = self._make_request("POST", "editPage", data) + self._make_request("POST", "editPage", data) # Verify deletion by checking the latest content. - latest_content = self.get_page(path) - # Compare the content as lists of dictionaries - return latest_content == expected_content + latest_page = self.get_page(path) + # Compare the content as lists of dictionaries. + return bool( + latest_page.get("success") + and latest_page.get("title") == "404" + and latest_page.get("content") == expected_content + ) def get_page( self, @@ -365,9 +385,11 @@ def get_page( result["attempts"] += 1 try: api_result: Dict[str, Any] = self._make_request("GET", "getPage", data) + result.update(api_result) result["success"] = True result["content"] = api_result.get("content", []) result["title"] = api_result.get("title") + result["error"] = None return result except Exception as e: result["error"] = str(e)