From 6ea4ec64e1d3f9a26a0f1d4440b22754b1695db4 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Wed, 29 Jan 2025 09:35:26 -0500 Subject: [PATCH] Remove embed API v2 code (#11945) :wastebasket: --- readthedocs/api/mixins.py | 20 +- .../embed/tests/data/sphinx/latest/index.html | 424 ------------------ .../embed/tests/data/sphinx/latest/index.json | 46 -- ...-or-environment-variables-in-my-build.html | 5 - .../sphinx/latest/page-sub-title-one.html | 20 - .../data/sphinx/latest/page-subsub-title.html | 15 - .../data/sphinx/latest/page-title-one.html | 95 ---- .../embed/tests/data/sphinx/latest/page.html | 103 ----- .../embed/tests/data/sphinx/latest/page.json | 59 --- readthedocs/embed/tests/test_api.py | 280 +----------- readthedocs/embed/v3/views.py | 3 - readthedocs/embed/views.py | 330 +------------- 12 files changed, 21 insertions(+), 1379 deletions(-) delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/index.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/index.json delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page-i-need-secrets-or-environment-variables-in-my-build.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page-sub-title-one.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page-subsub-title.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page-title-one.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page.html delete mode 100644 readthedocs/embed/tests/data/sphinx/latest/page.json diff --git a/readthedocs/api/mixins.py b/readthedocs/api/mixins.py index 9ef7a2f648a..3bc1cde5b91 100644 --- a/readthedocs/api/mixins.py +++ b/readthedocs/api/mixins.py @@ -2,12 +2,10 @@ import structlog from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from readthedocs.core.unresolver import UnresolverError, unresolve from readthedocs.core.utils import get_cache_tag -from readthedocs.projects.models import Project from readthedocs.proxito.cache import add_cache_tags log = structlog.get_logger(__name__) @@ -77,11 +75,6 @@ class EmbedAPIMixin: to avoid hitting the database multiple times on the same request. """ - # This class is shared between EmbedAPI v2 and v3. - # In v3, we only support the `url` parameter, - # but in v2 we support `project` and `version` as well. - support_url_parameter_only = False - @cached_property def unresolved_url(self): url = self.request.GET.get("url") @@ -102,11 +95,7 @@ def _get_project(self): if self.unresolved_url: return self.unresolved_url.project - if self.support_url_parameter_only: - raise Http404 - - project_slug = self.request.GET.get("project") - return get_object_or_404(Project, slug=project_slug) + raise Http404 @functools.lru_cache(maxsize=1) def _get_version(self): @@ -116,9 +105,4 @@ def _get_version(self): if self.unresolved_url: return self.unresolved_url.version - if self.support_url_parameter_only: - raise Http404 - - version_slug = self.request.GET.get("version", "latest") - project = self._get_project() - return get_object_or_404(project.versions.all(), slug=version_slug) + raise Http404 diff --git a/readthedocs/embed/tests/data/sphinx/latest/index.html b/readthedocs/embed/tests/data/sphinx/latest/index.html deleted file mode 100644 index 7d205053b9b..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/index.html +++ /dev/null @@ -1,424 +0,0 @@ -
-

Welcome to Read the Docs

-

- Read the Docs - hosts documentation for the open source community. - We support - Sphinx - docs written with reStructuredText and CommonMark. - We pull your code from your Subversion, - Bazaar, - Git, - and Mercurial repositories. - Then we build documentation and host it for you. - Think of it as Continuous Documentation. -

-

- The code is open source, and available on GitHub. -

-

- The main documentation for the site is organized into a couple sections: -

- -

Information about development is also available:

- -
-

- User Documentation

- -
- - - -
-

- Business Documentation -

- -
-
-

- Custom Install Documentation -

- -
- -
diff --git a/readthedocs/embed/tests/data/sphinx/latest/index.json b/readthedocs/embed/tests/data/sphinx/latest/index.json deleted file mode 100644 index ad5ffa170b1..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/index.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "body": "", - "alabaster_version": "0.7.10", - "display_toc": false, - "title": "Welcome to Read the Docs", - "sourcename": "index.rst.txt", - "customsidebar": null, - "metatags": "", - "current_page_name": "index", - "next": { - "link": "getting_started/", - "title": "Getting Started" - }, - "rellinks": [ - [ - "genindex", - "General Index", - "I", - "index" - ], - [ - "http-routingtable", - "HTTP Routing Table", - "", - "routing table" - ], - [ - "py-modindex", - "Python Module Index", - "", - "modules" - ], - [ - "getting_started", - "Getting Started", - "N", - "next" - ] - ], - "meta": {}, - "parents": [], - "sidebars": null, - "toc": "\n", - "prev": null, - "page_source_suffix": ".rst" -} diff --git a/readthedocs/embed/tests/data/sphinx/latest/page-i-need-secrets-or-environment-variables-in-my-build.html b/readthedocs/embed/tests/data/sphinx/latest/page-i-need-secrets-or-environment-variables-in-my-build.html deleted file mode 100644 index 8906924db65..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page-i-need-secrets-or-environment-variables-in-my-build.html +++ /dev/null @@ -1,5 +0,0 @@ -
-

I Need Secrets (or Environment Variables) in my Build

-

It may happen that your documentation depends on an authenticated service to be built properly.

-
diff --git a/readthedocs/embed/tests/data/sphinx/latest/page-sub-title-one.html b/readthedocs/embed/tests/data/sphinx/latest/page-sub-title-one.html deleted file mode 100644 index 73689d80035..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page-sub-title-one.html +++ /dev/null @@ -1,20 +0,0 @@ -
-

Sub-title one§

-

Sub title

- -
-

Subsub title

-

This is a H3 title.

- - -
- - Search analytics demo - -

- Fig. 4 I'm a figure! - -

-
-
-
diff --git a/readthedocs/embed/tests/data/sphinx/latest/page-subsub-title.html b/readthedocs/embed/tests/data/sphinx/latest/page-subsub-title.html deleted file mode 100644 index 04096e7540a..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page-subsub-title.html +++ /dev/null @@ -1,15 +0,0 @@ -
-

Subsub title

-

This is a H3 title.

- - -
- - Search analytics demo - -

- Fig. 4 I'm a figure! - -

-
-
diff --git a/readthedocs/embed/tests/data/sphinx/latest/page-title-one.html b/readthedocs/embed/tests/data/sphinx/latest/page-title-one.html deleted file mode 100644 index f48296e3828..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page-title-one.html +++ /dev/null @@ -1,95 +0,0 @@ -
-

Title One

-

This is another H1 title.

- -
-

Sub-title one§

-

Sub title

- -
-

Subsub title

-

This is a H3 title.

- - -
- - Search analytics demo - -

- Fig. 4 I'm a figure! - -

-
-
-
- - -
-

Adding a new scenario to the repository

-

- Sphinx configuration file used to build this docs: -

- - -
- - - - - - -
-
-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-
-
-
-
# -*- coding: utf-8 -*-
-
-# Default settings
-project = 'Test Builds'
-extensions = [
-    'sphinx_autorun',
-]
-
-latex_engine = 'xelatex'  # allow us to build Unicode chars
-
-
-# Include all your settings here
-html_theme = 'sphinx_rtd_theme'
-              
-
-
-
- -
- - -
-
-
>>> # Build at
->>> import datetime
->>> datetime.datetime.utcnow()  # UTC
-datetime.datetime(2020, 5, 3, 16, 38, 11, 137311)
-        
-
-
-
- -
diff --git a/readthedocs/embed/tests/data/sphinx/latest/page.html b/readthedocs/embed/tests/data/sphinx/latest/page.html deleted file mode 100644 index db00c71d171..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page.html +++ /dev/null @@ -1,103 +0,0 @@ -

Content at the beginning.

- -
-

I Need Secrets (or Environment Variables) in my Build

-

It may happen that your documentation depends on an authenticated service to be built properly.

-
- -
-

Title One

-

This is another H1 title.

- -
-

Sub-title one§

-

Sub title

- -
-

Subsub title

-

This is a H3 title.

- - -
- - Search analytics demo - -

- Fig. 4 I'm a figure! - -

-
-
-
- - -
-

Adding a new scenario to the repository

-

- Sphinx configuration file used to build this docs: -

- - -
- - - - - - -
-
-
 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-
-
-
-
# -*- coding: utf-8 -*-
-
-# Default settings
-project = 'Test Builds'
-extensions = [
-    'sphinx_autorun',
-]
-
-latex_engine = 'xelatex'  # allow us to build Unicode chars
-
-
-# Include all your settings here
-html_theme = 'sphinx_rtd_theme'
-              
-
-
-
- -
- - -
-
-
>>> # Build at
->>> import datetime
->>> datetime.datetime.utcnow()  # UTC
-datetime.datetime(2020, 5, 3, 16, 38, 11, 137311)
-        
-
-
-
- -
diff --git a/readthedocs/embed/tests/data/sphinx/latest/page.json b/readthedocs/embed/tests/data/sphinx/latest/page.json deleted file mode 100644 index 464200077c7..00000000000 --- a/readthedocs/embed/tests/data/sphinx/latest/page.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "parents": [ - { - "link": "../", - "title": "Guides" - } - ], - "prev": { - "link": "../conda/", - "title": "Conda Support" - }, - "next": { - "link": "../feature-flags/", - "title": "Feature Flags" - }, - "title": "I Need Secrets (or Environment Variables) in my Build", - "meta": {}, - "body": "", - "metatags": "", - "rellinks": [ - [ - "genindex", - "General Index", - "I", - "index" - ], - [ - "http-routingtable", - "HTTP Routing Table", - "", - "routing table" - ], - [ - "guides/feature-flags", - "Feature Flags", - "N", - "next" - ], - [ - "guides/conda", - "Conda Support", - "P", - "previous" - ] - ], - "sourcename": "guides/environment-variables.rst.txt", - "toc": "\n", - "display_toc": true, - "page_source_suffix": ".rst", - "current_page_name": "guides/environment-variables", - "sidebars": [ - "localtoc.html", - "relations.html", - "sourcelink.html", - "searchbox.html" - ], - "customsidebar": null, - "alabaster_version": "0.7.12" -} diff --git a/readthedocs/embed/tests/test_api.py b/readthedocs/embed/tests/test_api.py index 50be6c5dd18..4a3396e1c77 100644 --- a/readthedocs/embed/tests/test_api.py +++ b/readthedocs/embed/tests/test_api.py @@ -1,22 +1,14 @@ -import json -from contextlib import contextmanager -from pathlib import Path -from unittest import mock - import pytest from django.urls import reverse from django_dynamic_fixture import get -from pyquery import PyQuery from rest_framework import status from readthedocs.builds.constants import LATEST -from readthedocs.projects.constants import MKDOCS, PUBLIC +from readthedocs.projects.constants import PUBLIC from readthedocs.projects.models import Project from readthedocs.subscriptions.constants import TYPE_EMBED_API from readthedocs.subscriptions.products import RTDProductFeature -data_path = Path(__file__).parent.resolve() / "data" - @pytest.mark.django_db class BaseTestEmbedAPI: @@ -42,274 +34,12 @@ def get(self, client, *args, **kwargs): """Wrapper around ``client.get`` to be overridden in the proxied api tests.""" return client.get(*args, **kwargs) - def _mock_open(self, content): - @contextmanager - def f(*args, **kwargs): - read_mock = mock.MagicMock() - read_mock.read.return_value = content - yield read_mock - - return f - - def _patch_sphinx_json_file(self, storage_mock, json_file, html_file): - storage_mock.exists.return_value = True - html_content = data_path / html_file - json_content = json.load(json_file.open()) - json_content["body"] = html_content.open().read() - storage_mock.open.side_effect = self._mock_open(json.dumps(json_content)) - - def _get_html_content(self, html_file): - section_content = [PyQuery(html_file.open().read()).outerHtml()] - return section_content - - def test_invalid_arguments(self, client): - query_params = ( - { - "project": self.project.slug, - "version": self.version.slug, - }, - { - "project": self.project.slug, - "version": self.version.slug, - "section": "foo", - }, - ) - - api_endpoint = reverse("embed_api") - for param in query_params: - r = self.get(client, api_endpoint, param) - assert r.status_code == status.HTTP_400_BAD_REQUEST - - @mock.patch("readthedocs.embed.views.build_media_storage") - def test_valid_arguments(self, storage_mock, client): - json_file = data_path / "sphinx/latest/page.json" - html_file = data_path / "sphinx/latest/page.html" - - self._patch_sphinx_json_file( - storage_mock=storage_mock, - json_file=json_file, - html_file=html_file, - ) - - query_params = ( - # URL only - {"url": "https://project.readthedocs.io/en/latest/index.html#title-one"}, - {"url": "http://project.readthedocs.io/en/latest/index.html#title-one"}, - {"url": "http://project.readthedocs.io/en/latest/#title-one"}, - { - "url": "http://project.readthedocs.io/en/latest/index.html?foo=bar#title-one" - }, - {"url": "http://project.readthedocs.io/en/latest/?foo=bar#title-one"}, - # doc & path - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "section": "title-one", - }, - { - "project": self.project.slug, - "version": self.version.slug, - "path": "/index.html", - "section": "title-one", - }, - { - "project": self.project.slug, - "version": self.version.slug, - "doc": "index", - "section": "title-one", - }, - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "doc": "index", - "section": "title-one", - }, - ) - api_endpoint = reverse("embed_api") - for param in query_params: - r = self.get(client, api_endpoint, param) - assert r.status_code == status.HTTP_200_OK - - def test_no_access(self, client, settings): - settings.RTD_DEFAULT_FEATURES = {} - api_endpoint = reverse("embed_api") - r = self.get( - client, - api_endpoint, - {"url": "https://project.readthedocs.io/en/latest/index.html#title-one"}, - ) - assert r.status_code == status.HTTP_403_FORBIDDEN - - @mock.patch("readthedocs.embed.views.build_media_storage") - def test_embed_unknown_section(self, storage_mock, client): - json_file = data_path / "sphinx/latest/index.json" - html_file = data_path / "sphinx/latest/index.html" - - self._patch_sphinx_json_file( - storage_mock=storage_mock, - json_file=json_file, - html_file=html_file, - ) - - response = self.get( - client, - reverse("embed_api"), - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "section": "Features", - }, - ) - - expected = { - "content": [], - "headers": [ - {"Welcome to Read the Docs": "#"}, - ], - "url": "http://project.readthedocs.io/en/latest/index.html", - "meta": { - "project": "project", - "version": "latest", - "doc": "index", - "section": "Features", - }, - } - - assert response.status_code == status.HTTP_200_OK - assert response.data == expected - - @pytest.mark.parametrize( - "path, docname", - [ - ("index.html", "index"), - ("index/", "index"), - ("index//", "index"), - ("/index.html", "index"), - ("/index/", "index"), - ("/index//", "index"), - ("guides/users/index.html", "guides/users/index"), - ("guides/users/", "guides/users"), - ("/guides/users/", "guides/users"), - ], - ) - @mock.patch("readthedocs.embed.views.build_media_storage") - def test_embed_dir_path(self, storage_mock, path, docname, client): - json_file = data_path / "sphinx/latest/page.json" - html_file = data_path / "sphinx/latest/page.html" - - self._patch_sphinx_json_file( - storage_mock=storage_mock, - json_file=json_file, - html_file=html_file, - ) - - response = self.get( - client, - reverse("embed_api"), - { - "project": self.project.slug, - "version": self.version.slug, - "path": path, - "section": "title-one", - }, - ) - - assert response.status_code == status.HTTP_200_OK - assert response.data["meta"]["doc"] == docname - - @pytest.mark.parametrize( - "section", - [ - "i-need-secrets-or-environment-variables-in-my-build", - "title-one", - "sub-title-one", - "subsub-title", - ], - ) - @mock.patch("readthedocs.embed.views.build_media_storage") - def test_embed_sphinx(self, storage_mock, section, client): - json_file = data_path / "sphinx/latest/page.json" - html_file = data_path / "sphinx/latest/page.html" - - self._patch_sphinx_json_file( - storage_mock=storage_mock, - json_file=json_file, - html_file=html_file, - ) - - response = self.get( - client, - reverse("embed_api"), - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "section": section, - }, - ) - - section_content = self._get_html_content( - data_path / f"sphinx/latest/page-{section}.html" - ) - - expected = { - "content": section_content, - "headers": [ - # TODO: return the full id here - {"I Need Secrets (or Environment Variables) in my Build": "#"}, - {"Title One": "#title-one"}, - {"Sub-title one": "#sub-title-one"}, - {"Subsub title": "#subsub-title"}, - # TODO: detect this header - # {'Adding a new scenario to the repository': 'adding-a-new-scenario-to-the-repository'} - ], - "url": "http://project.readthedocs.io/en/latest/index.html", - "meta": { - "project": "project", - "version": "latest", - "doc": "index", - "section": section, - }, - } - - assert response.status_code == status.HTTP_200_OK - assert response.data == expected - assert response["Cache-tag"] == "project,project:latest" - - def test_embed_mkdocs(self, client): - """API v2 doesn't support mkdocs.""" - self.version.documentation_type = MKDOCS - self.version.save() - - response = self.get( - client, - reverse("embed_api"), - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "section": "Installation", - }, - ) - - assert response.status_code == status.HTTP_404_NOT_FOUND - - def test_no_access(self, client, settings): - settings.RTD_DEFAULT_FEATURES = {} + def test_is_deprecated(self, client): response = self.get( - client, - reverse("embed_api"), - { - "project": self.project.slug, - "version": self.version.slug, - "path": "index.html", - "section": "title-one", - }, + client=client, + path=reverse("embed_api"), ) - assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.status_code == status.HTTP_410_GONE class TestEmbedAPI(BaseTestEmbedAPI): diff --git a/readthedocs/embed/v3/views.py b/readthedocs/embed/v3/views.py index 08457d3d0a8..3170a70a176 100644 --- a/readthedocs/embed/v3/views.py +++ b/readthedocs/embed/v3/views.py @@ -61,9 +61,6 @@ class EmbedAPIBase(EmbedAPIMixin, CDNCacheTagsMixin, APIView): permission_classes = [HasEmbedAPIAccess, IsAuthorizedToGetContenFromVersion] renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - # API V3 doesn't allow passing a version or project query parameters. - support_url_parameter_only = True - @property def external(self): # NOTE: ``readthedocs.core.unresolver.unresolve`` returns ``None`` when diff --git a/readthedocs/embed/views.py b/readthedocs/embed/views.py index b8207d95d38..3c61a252551 100644 --- a/readthedocs/embed/views.py +++ b/readthedocs/embed/views.py @@ -1,329 +1,27 @@ """Views for the embed app.""" -import datetime -import json -import re - -import pytz import structlog -from django.conf import settings -from django.template.defaultfilters import slugify -from docutils.nodes import make_id -from pyquery import PyQuery as PQ # noqa from rest_framework import status -from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer +from rest_framework.permissions import AllowAny +from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView -from readthedocs.api.mixins import CDNCacheTagsMixin, EmbedAPIMixin -from readthedocs.api.v2.permissions import IsAuthorizedToViewVersion -from readthedocs.api.v3.permissions import HasEmbedAPIAccess -from readthedocs.core.resolver import Resolver -from readthedocs.embed.utils import clean_references, recurse_while_none -from readthedocs.storage import build_media_storage +from readthedocs.api.mixins import EmbedAPIMixin log = structlog.get_logger(__name__) -def escape_selector(selector): - """Escape special characters from the section id.""" - regex = re.compile(r'(!|"|#|\$|%|\'|\(|\)|\*|\+|\,|\.|\/|\:|\;|\?|@)') - ret = re.sub(regex, r"\\\1", selector) - return ret - - -class EmbedAPI(EmbedAPIMixin, CDNCacheTagsMixin, APIView): - # pylint: disable=line-too-long - - """ - Embed a section of content from any Read the Docs page. - - Returns headers and content that matches the queried section. - - ### Arguments - - We support two different ways to query the API: - - * project (required) - * version (required) - * doc or path (required) - * section - - or: - - * url (with fragment) (required) - - ### Example - - - GET https://readthedocs.org/api/v2/embed/?project=requestsF&version=latest&doc=index§ion=User%20Guide&path=/index.html - - GET https://readthedocs.org/api/v2/embed/?url=https://docs.readthedocs.io/en/latest/features.html%23github-bitbucket-and-gitlab-integration - - # Current Request - """ # noqa - - permission_classes = [HasEmbedAPIAccess, IsAuthorizedToViewVersion] - renderer_classes = [JSONRenderer, BrowsableAPIRenderer] - - @property - def external(self): - # Always return False because APIv2 does not support external domains - return False - - def _is_disabled_for_deprecation(self): - if not settings.RTD_ENFORCE_BROWNOUTS_FOR_DEPRECATIONS: - return False - - tzinfo = pytz.timezone("America/Los_Angeles") - now = datetime.datetime.now(tz=tzinfo) - # Dates as per https://about.readthedocs.com/blog/2024/11/embed-api-v2-deprecated/. - # fmt: off - is_disabled = ( - # 12 hours brownout. - datetime.datetime(2024, 12, 9, 0, 0, 0, tzinfo=tzinfo) < now < datetime.datetime(2024, 12, 9, 12, 0, 0, tzinfo=tzinfo) - # 24 hours brownout. - or datetime.datetime(2025, 1, 13, 0, 0, 0, tzinfo=tzinfo) < now < datetime.datetime(2025, 1, 14, 0, 0, 0, tzinfo=tzinfo) - # Permanent removal. - or datetime.datetime(2025, 1, 20, 0, 0, 0, tzinfo=tzinfo) < now - ) - # fmt: on - return is_disabled +class EmbedAPI(EmbedAPIMixin, APIView): + permission_classes = [AllowAny] + renderer_classes = [JSONRenderer] def get(self, request): - """Handle the get request.""" - - if self._is_disabled_for_deprecation(): - return Response( - { - "error": ( - "Embed API v2 has been deprecated and is no longer available, please use embed API v3 instead. " - "Read our blog post for more information: https://about.readthedocs.com/blog/2024/11/embed-api-v2-deprecated/." - ) - }, - status=status.HTTP_410_GONE, - ) - - project = self._get_project() - version = self._get_version() - - url = request.GET.get("url") - path = request.GET.get("path", "") - doc = request.GET.get("doc") - section = request.GET.get("section") - - if url: - unresolved = self.unresolved_url - path = unresolved.filename - section = unresolved.parsed_url.fragment - elif not path and not doc: - return Response( - { - "error": ( - "Invalid Arguments. " - 'Please provide "url" or "section" and "path" GET arguments.' - ) - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Generate the docname from path - # by removing the ``.html`` extension and trailing ``/``. - if path: - doc = re.sub(r"(.+)\.html$", r"\1", path.strip("/")) - - response = do_embed( - project=project, - version=version, - doc=doc, - section=section, - path=path, - url=url, - ) - - if not response: - return Response( - { - "error": ( - "Can't find content for section: " - f"doc={doc} path={path} section={section}" - ) - }, - status=status.HTTP_404_NOT_FOUND, - ) - - log.info( - "EmbedAPI successful response.", - project_slug=project.slug, - version_slug=version.slug, - doc=doc, - section=section, - path=path, - url=url, - referer=request.headers.get("Referer"), - hoverxref_version=request.headers.get("X-Hoverxref-Version"), + return Response( + { + "error": ( + "Embed API v2 has been deprecated and is no longer available, please use embed API v3 instead. " + "Read our blog post for more information: https://about.readthedocs.com/blog/2024/11/embed-api-v2-deprecated/." + ) + }, + status=status.HTTP_410_GONE, ) - return Response(response) - - -def do_embed(*, project, version, doc=None, path=None, section=None, url=None): - """Get the embed response from a document section.""" - if not url: - url = Resolver().resolve_version( - project=project, - version=version, - filename=path or doc, - ) - - content = None - headers = None - # Embed API v2 supports Sphinx only. - if version.is_sphinx_type: - file_content = _get_doc_content( - project=project, - version=version, - doc=doc, - ) - if not file_content: - return None - - content, headers, section = parse_sphinx( - content=file_content, - section=section, - url=url, - ) - else: - log.info("Using EmbedAPIv2 for a non Sphinx project.") - return None - - if content is None: - return None - - return { - "content": content, - "headers": headers, - "url": url, - "meta": { - "project": project.slug, - "version": version.slug, - "doc": doc, - "section": section, - }, - } - - -def _get_doc_content(project, version, doc): - storage_path = project.get_storage_path( - "json", - version_slug=version.slug, - include_file=False, - version_type=version.type, - ) - file_path = build_media_storage.join( - storage_path, - f"{doc}.fjson".lstrip("/"), - ) - try: - with build_media_storage.open(file_path) as file: - return json.load(file) - except Exception: # noqa - log.warning("Unable to read file.", file_path=file_path) - - return None - - -def parse_sphinx(content, section, url): - """Get the embed content for the section.""" - body = content.get("body") - toc = content.get("toc") - - if not content or not body or not toc: - return (None, None, section) - - headers = [recurse_while_none(element) for element in PQ(toc)("a")] - - if not section and headers: - # If no section is sent, return the content of the first one - # TODO: This will always be the full page content, - # lets do something smarter here - section = list(headers[0].keys())[0].lower() - - if not section: - return [], headers, None - - body_obj = PQ(body) - escaped_section = escape_selector(section) - - elements_id = [ - escaped_section, - slugify(escaped_section), - make_id(escaped_section), - f"module-{escaped_section}", - ] - query_result = [] - for element_id in elements_id: - if not element_id: - continue - try: - query_result = body_obj(f"#{element_id}") - if query_result: - break - except Exception: # noqa - log.info( - "Failed to query section.", - url=url, - element_id=element_id, - ) - - if not query_result: - selector = f':header:contains("{escaped_section}")' - query_result = body_obj(selector).parent() - - # Handle ``dt`` special cases - if len(query_result) == 1 and query_result[0].tag == "dt": - parent = query_result.parent() - if "glossary" in parent.attr("class"): - # Sphinx HTML structure for term glossary puts the ``id`` in the - # ``dt`` element with the title of the term. In this case, we - # need to return the next sibling which contains the definition - # of the term itself. - - # Structure: - #
- #
definition
- #
Text definition for the term
- # ... - #
- query_result = query_result.next() - elif "citation" in parent.attr("class"): - # Sphinx HTML structure for sphinxcontrib-bibtex puts the ``id`` in the - # ``dt`` element with the title of the cite. In this case, we - # need to return the next sibling which contains the cite itself. - - # Structure: - #
- #
Title of the cite
- #
Content of the cite
- # ... - #
- query_result = query_result.next() - else: - # Sphinx HTML structure for definition list puts the ``id`` - # the ``dt`` element, instead of the ``dl``. This makes - # the backend to return just the title of the definition. If we - # detect this case, we return the parent (the whole ``dl``) - - # Structure: - #
- #
- # config - #
- #

Text with a description

- #
- query_result = parent - - def dump(obj): - """Handle API-based doc HTML.""" - if obj[0].tag in ["span", "h2"]: - return obj.parent().outerHtml() - return obj.outerHtml() - - ret = [dump(clean_references(obj, url)) for obj in query_result] - return ret, headers, section