From ee8752ada97955f2f53d1c917d1b3ebcf2074a0e Mon Sep 17 00:00:00 2001 From: Manabu Niseki Date: Tue, 24 Feb 2026 17:11:00 +0900 Subject: [PATCH] feat: support visibility update & reset --- src/urlscan/pro/__init__.py | 20 +++++++++++ src/urlscan/pro/visibility.py | 43 ++++++++++++++++++++++++ src/urlscan/types.py | 1 + tests/integration/pro/test_visibility.py | 19 +++++++++++ tests/unit/pro/test_visibility.py | 38 +++++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 src/urlscan/pro/visibility.py create mode 100644 tests/integration/pro/test_visibility.py create mode 100644 tests/unit/pro/test_visibility.py diff --git a/src/urlscan/pro/__init__.py b/src/urlscan/pro/__init__.py index 375a705..3ac11de 100644 --- a/src/urlscan/pro/__init__.py +++ b/src/urlscan/pro/__init__.py @@ -5,6 +5,7 @@ from urlscan.client import BaseClient from urlscan.iterator import SearchIterator +from urlscan.pro.visibility import Visibility from urlscan.utils import _compact from .brand import Brand @@ -153,6 +154,25 @@ def subscription(self) -> Subscription: retry=self._retry, ) + @cached_property + def visibility(self) -> Visibility: + """Visibility API client instance. + + Returns: + Visibility: Visibility API client instance. + + """ + return Visibility( + api_key=self._api_key, + base_url=self._base_url, + user_agent=self._user_agent, + trust_env=self._trust_env, + timeout=self._timeout, + proxy=self._proxy, + verify=self._verify, + retry=self._retry, + ) + def structure_search( self, scan_id: str, diff --git a/src/urlscan/pro/visibility.py b/src/urlscan/pro/visibility.py new file mode 100644 index 0000000..e498853 --- /dev/null +++ b/src/urlscan/pro/visibility.py @@ -0,0 +1,43 @@ +"""Visibility API client module.""" + +from urlscan.client import BaseClient +from urlscan.types import UpdateVisibilityType + + +class Visibility(BaseClient): + """Visibility API client.""" + + def update(self, uuid: str, visibility: UpdateVisibilityType) -> dict: + """Update visibility of a scan owned by you or your team. + + Args: + uuid (str): The UUID of a scan. + visibility (UpdateVisibilityType): The new visibility of the scan result: public, unlisted, private, deleted + + Reference: + https://docs.urlscan.io/apis/urlscan-openapi/scanning/updateresultvisibility + + """ + if visibility not in ["public", "private", "unlisted", "deleted"]: + raise ValueError( + "Visibility must be either 'public', 'private', 'unlisted', or 'deleted'" + ) + + res = self._put( + f"/api/v1/result/{uuid}/visibility/", + json={"visibility": visibility}, + ) + return self._response_to_json(res) + + def reset(self, uuid: str) -> dict: + """Reset the visibility of a scan owned by you or your team to its original visibility. + + Args: + uuid (str): The UUID of a scan. + + Reference: + https://docs.urlscan.io/apis/urlscan-openapi/scanning/deleteresultvisibility + + """ + res = self._delete(f"/api/v1/result/{uuid}/visibility/") + return self._response_to_json(res) diff --git a/src/urlscan/types.py b/src/urlscan/types.py index 0bdf28c..dba64ef 100644 --- a/src/urlscan/types.py +++ b/src/urlscan/types.py @@ -6,6 +6,7 @@ "scans", "hostnames", "incidents", "notifications", "certificates" ] VisibilityType = Literal["public", "private", "unlisted"] +UpdateVisibilityType = VisibilityType | Literal["deleted"] SearchType = Literal["search"] RetrieveType = Literal["retrieve"] ActionType = VisibilityType | SearchType | RetrieveType diff --git a/tests/integration/pro/test_visibility.py b/tests/integration/pro/test_visibility.py new file mode 100644 index 0000000..ed0d590 --- /dev/null +++ b/tests/integration/pro/test_visibility.py @@ -0,0 +1,19 @@ +import pytest + +from urlscan import Client, Pro + + +@pytest.mark.integration +def test_change_and_reset_visibility(client: Client, pro: Pro, url: str): + result = client.scan(url, visibility="private") + uuid: str = result["uuid"] + client.wait_for_result(uuid) + + # Change visibility to public + res = pro.visibility.update(uuid, "public") + assert res["uuid"] == uuid + assert res["visibility"] == "public" + + # Reset visibility to original (private) + res = pro.visibility.reset(uuid) + assert res["uuid"] == uuid diff --git a/tests/unit/pro/test_visibility.py b/tests/unit/pro/test_visibility.py new file mode 100644 index 0000000..2d07a07 --- /dev/null +++ b/tests/unit/pro/test_visibility.py @@ -0,0 +1,38 @@ +from typing import Any + +import pytest +from pytest_httpserver import HTTPServer + +from urlscan.pro import Pro + + +def test_update(pro: Pro, httpserver: HTTPServer): + expected: dict[str, Any] = { + "uuid": "dummy", + "message": "Visibility updated", + "visibility": "public", + } + httpserver.expect_request( + "/api/v1/result/dummy/visibility/", method="PUT", json={"visibility": "public"} + ).respond_with_json(expected) + + got = pro.visibility.update("dummy", "public") + assert got == expected + + +def test_update_with_invalid_visibility(pro: Pro): + with pytest.raises(ValueError): + pro.visibility.update("dummy", "invalid") # type: ignore + + +def test_reset(pro: Pro, httpserver: HTTPServer): + expected: dict[str, Any] = { + "uuid": "dummy", + "message": "Visibility reset to public", + } + httpserver.expect_request( + "/api/v1/result/dummy/visibility/", method="DELETE" + ).respond_with_json(expected) + + got = pro.visibility.reset("dummy") + assert got == expected