From 50186ed9f2192c29d7fff58c40b43df8284f828e Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Sun, 28 Sep 2025 11:29:30 -0300 Subject: [PATCH 1/8] feat!: change PlanData.duration type for dict[str, Any] to int --- squarecloud/data.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/squarecloud/data.py b/squarecloud/data.py index e698b8d..8b821e7 100644 --- a/squarecloud/data.py +++ b/squarecloud/data.py @@ -40,7 +40,7 @@ class PlanData(BaseDataClass): name: str memory: dict[str, Any] - duration: dict[str, Any] | None + duration: int | None class Language(BaseDataClass): @@ -139,6 +139,7 @@ class UserData(BaseDataClass): name: str plan: PlanData email: str | None = None + locale: str | None = None class LogsData(BaseDataClass): @@ -306,13 +307,13 @@ def date_time(self) -> datetime: return datetime.fromisoformat(self.date) class ExtraBaseAnalytics(BaseAnalytics): type: str - class Visits(BaseAnalytics): + class Visits(BaseAnalytics): pass - class Countries(ExtraBaseAnalytics): + class Countries(ExtraBaseAnalytics): pass class Devices(ExtraBaseAnalytics): pass - class Os(ExtraBaseAnalytics): + class Os(ExtraBaseAnalytics): pass class Browsers(ExtraBaseAnalytics): pass From 0c1b8219f661d3a5b783b551b0bca81bcf71a8ca Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Sun, 28 Sep 2025 11:31:51 -0300 Subject: [PATCH 2/8] feat: add snapshots property to AppCache --- squarecloud/app.py | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/squarecloud/app.py b/squarecloud/app.py index aae0d59..5caa305 100644 --- a/squarecloud/app.py +++ b/squarecloud/app.py @@ -42,6 +42,7 @@ class AppCache: '_logs', '_backup', '_app_data', + '_snapshot' ) def __init__(self) -> None: @@ -56,6 +57,7 @@ def __init__(self) -> None: self._status: StatusData | None = None self._logs: LogsData | None = None self._backup: Snapshot | None = None + self._snapshot: Snapshot | None = None self._app_data: AppData | None = None @property @@ -92,6 +94,17 @@ def backup(self) -> Snapshot: """ return self._backup + @property + def snapshot(self) -> Snapshot: + """ + The snapshot method is a property that returns the cached Snapshot of + the application. + + :return: The value of the _snapshot attribute + :rtype: Snapshot + """ + return self._snapshot + @property def app_data(self) -> AppData: """ @@ -135,6 +148,7 @@ def update(self, *args) -> None: self._logs = arg elif isinstance(arg, Snapshot): self._backup = arg + self._snapshot = arg elif isinstance(arg, AppData): self._app_data = arg else: @@ -473,7 +487,7 @@ async def backup(self, *_args, **_kwargs) -> Snapshot: """ backup: Snapshot = await self.client.snapshot(self.id) return backup - + @_update_cache @_notify_listener(Endpoint.snapshot()) async def snapshot(self, *_args, **_kwargs) -> Snapshot: @@ -657,7 +671,7 @@ async def github_integration(self, access_token: str) -> str: async def domain_analytics(self) -> DomainAnalytics: """ Retrieve analytics data for the application's domain. - + :param self: Refer to the instance of the class. :returns: An instance of :class:`DomainAnalytics` containing analytics data for the domain. :rtype: DomainAnalytics @@ -686,11 +700,11 @@ async def set_custom_domain(self, custom_domain: str) -> Response: async def all_backups(self) -> list[SnapshotInfo]: backups: list[SnapshotInfo] = await self.client.all_app_snapshots(self.id) return backups - + async def all_snapshots(self) -> list[SnapshotInfo]: """ Retrieve all snapshots of the application. - + :return: A list of SnapshotInfo objects representing all snapshots of the application. :rtype: list[SnapshotInfo] """ @@ -701,7 +715,7 @@ async def all_snapshots(self) -> list[SnapshotInfo]: async def move_file(self, origin: str, dest: str) -> Response: """ Moves a file from the origin path to the destination path within the application. - + :param origin: The source path of the file to be moved. :type origin: str :param dest: The destination path where the file should be moved. @@ -709,7 +723,7 @@ async def move_file(self, origin: str, dest: str) -> Response: :return: A Response object containing the result of the file move operation. :rtype: Response """ - + return await self.client.move_app_file(self.id, origin, dest) async def current_integration(self) -> Response: @@ -719,13 +733,13 @@ async def current_integration(self) -> Response: async def dns_records(self) -> list[DNSRecord]: """ Retrieve the DNS records associated with the application. - + :returns: A list of DNSRecord objects representing the DNS records. :rtype: list[DNSRecord] """ - + return await self.client.dns_records(self.id) - + async def get_envs(self) -> dict[str, str]: """ Get environment variables of the application. @@ -745,7 +759,7 @@ async def set_envs(self, envs: dict[str, str]) -> dict[str,str]: :rtype: dict[str, str] """ return await self.client.set_app_envs(self.id, envs) - + async def delete_envs(self, keys: list[str]) -> dict[str,str]: """ Deletes environment variables from the application. @@ -756,7 +770,7 @@ async def delete_envs(self, keys: list[str]) -> dict[str,str]: :rtype: dict[str, str] """ return await self.client.delete_app_envs(self.id, keys) - + async def overwrite_env(self, envs: dict[str, str]) -> dict[str,str]: """ Overwrites the environment variables for the application. @@ -766,4 +780,4 @@ async def overwrite_env(self, envs: dict[str, str]) -> dict[str,str]: :return: A dictionary of the environment variables. :rtype: dict[str, str] """ - return await self.client.overwrite_app_envs(self.id, envs) \ No newline at end of file + return await self.client.overwrite_app_envs(self.id, envs) From 093b9a5103d829963c89ea2081f9d8034dee2e68 Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Sun, 28 Sep 2025 22:41:09 -0300 Subject: [PATCH 3/8] feat: add InvalidSubdomain exception and fix INVALID_DEPENDENCY key --- squarecloud/errors.py | 9 +++++++++ squarecloud/http/http_client.py | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/squarecloud/errors.py b/squarecloud/errors.py index 5cb8ed0..49e06c6 100644 --- a/squarecloud/errors.py +++ b/squarecloud/errors.py @@ -217,6 +217,15 @@ def __init__(self, domain: str, *args, **kwargs) -> None: self.message = f'"{domain}" is a invalid custom domain' +class InvalidSubdomain(RequestError): + """ + raised when an invalid subdomain is provided + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self.message = 'The provided subdomain is invalid or already in use.' + class InvalidStart(InvalidConfig): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) diff --git a/squarecloud/http/http_client.py b/squarecloud/http/http_client.py index 839f516..08d77ea 100644 --- a/squarecloud/http/http_client.py +++ b/squarecloud/http/http_client.py @@ -28,6 +28,7 @@ NotFoundError, RequestError, TooManyRequests, + InvalidSubdomain ) from ..logger import logger from .endpoints import Endpoint, Router @@ -82,7 +83,7 @@ def _get_error(code: str) -> type[RequestError] | None: 'FEW_MEMORY': FewMemory, 'BAD_MEMORY': BadMemory, 'MISSING_CONFIG': MissingConfigFile, - 'MISSING_DEPENDENCIES_FILE': MissingDependenciesFile, + 'INVALID_DEPENDENCY': MissingDependenciesFile, 'MISSING_MAIN': MissingMainFile, 'INVALID_MAIN': InvalidMain, 'INVALID_DISPLAY_NAME': InvalidDisplayName, @@ -94,11 +95,10 @@ def _get_error(code: str) -> type[RequestError] | None: 'INVALID_ACCESS_TOKEN': InvalidAccessToken, 'REGEX_VALIDATION': InvalidDomain, 'INVALID_START': InvalidStart, + 'INVALID_SUBDOMAIN': InvalidSubdomain } - error_class = errors.get(code, None) - if error_class is None: - return None - return error_class + return errors.get(code, None) + class HTTPClient: From c10831ddb0fced4690472751b678f428abe53188 Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Sun, 28 Sep 2025 22:43:33 -0300 Subject: [PATCH 4/8] refactor(test):rename Backup-related dataclasses and method to Snapshot --- tests/test_app.py | 20 ++++++++++---------- tests/test_app_data.py | 16 ++++++++-------- tests/test_capture_listeners.py | 14 +++++++------- tests/test_request_listeners.py | 30 +++++++++++++++--------------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index bf4e361..0096669 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,7 +3,7 @@ import pytest import squarecloud -from squarecloud import BackupInfo +from squarecloud import SnapshotInfo from squarecloud.app import Application from squarecloud.http import Response from tests import GITHUB_ACCESS_TOKEN @@ -24,12 +24,12 @@ async def test_app_status(self, app: Application): async def test_app_logs(self, app: Application): assert isinstance(await app.logs(), squarecloud.LogsData) - async def test_app_backup(self, app: Application): - assert isinstance(await app.backup(), squarecloud.Backup) + async def test_app_snapshot(self, app: Application): + assert isinstance(await app.snapshot(), squarecloud.Snapshot) - async def test_download_backup(self, app: Application): - backup = await app.backup() - zip_file = await backup.download() + async def test_download_snapshot(self, app: Application): + snapshot = await app.snapshot() + zip_file = await snapshot.download() assert isinstance(zip_file, ZipFile) async def test_app_github_integration(self, app: Application): @@ -49,10 +49,10 @@ async def test_domain_analytics(self, app: Application): async def test_set_custom_domain(self, app: Application): assert isinstance(await app.set_custom_domain('test.com.br'), str) - async def test_get_all_backups(self, app: Application): - backups = await app.all_backups() - assert isinstance(backups, list) - assert isinstance(backups[0], BackupInfo) + async def test_get_all_snapshots(self, app: Application): + snapshots = await app.all_snapshots() + assert isinstance(snapshots, list) + assert isinstance(snapshots[0], SnapshotInfo) async def test_move_file(self, app: Application): response = await app.move_file('main.py', 'test.py') diff --git a/tests/test_app_data.py b/tests/test_app_data.py index 1017939..a6c54b7 100644 --- a/tests/test_app_data.py +++ b/tests/test_app_data.py @@ -3,7 +3,7 @@ import pytest from squarecloud.app import Application -from squarecloud.data import Backup, LogsData, StatusData +from squarecloud.data import Snapshot, LogsData, StatusData @pytest.mark.asyncio(scope='session') @@ -25,19 +25,19 @@ async def test_status(self, app: Application): assert cache.status is None sleep(10) - async def test_backup(self, app: Application): + async def test_snapshot(self, app: Application): cache = app.cache - assert cache.backup is None + assert cache.snapshot is None - backup = await app.backup(update_cache=False) - assert cache.backup is None + snapshot = await app.snapshot(update_cache=False) + assert cache.snapshot is None - cache.update(backup) - assert isinstance(cache.backup, Backup) + cache.update(snapshot) + assert isinstance(cache.snapshot, Snapshot) cache.clear() - assert cache.backup is None + assert cache.snapshot is None async def test_logs(self, app: Application): cache = app.cache diff --git a/tests/test_capture_listeners.py b/tests/test_capture_listeners.py index b00a2aa..287b628 100644 --- a/tests/test_capture_listeners.py +++ b/tests/test_capture_listeners.py @@ -7,7 +7,7 @@ from squarecloud import Endpoint, errors from squarecloud.app import Application -from squarecloud.data import Backup, LogsData, StatusData +from squarecloud.data import Snapshot, LogsData, StatusData from squarecloud.listeners import Listener @@ -36,14 +36,14 @@ async def capture_status(before, after): await app.status() - @_clear_listener_on_rerun(Endpoint.backup()) - async def test_capture_backup(self, app: Application): - @app.capture(Endpoint.backup(), force_raise=True) - async def capture_backup(before, after): + @_clear_listener_on_rerun(Endpoint.snapshot()) + async def test_capture_snapshot(self, app: Application): + @app.capture(Endpoint.snapshot(), force_raise=True) + async def capture_snapshot(before, after): assert before is None - assert isinstance(after, Backup) + assert isinstance(after, Snapshot) - await app.backup() + await app.snapshot() @_clear_listener_on_rerun(Endpoint.logs()) async def test_capture_logs(self, app: Application): diff --git a/tests/test_request_listeners.py b/tests/test_request_listeners.py index c1571c0..a5a537e 100644 --- a/tests/test_request_listeners.py +++ b/tests/test_request_listeners.py @@ -9,8 +9,8 @@ from squarecloud import Client, Endpoint, File from squarecloud.app import Application from squarecloud.data import ( - Backup, - BackupInfo, + Snapshot, + SnapshotInfo, DeployData, DomainAnalytics, FileInfo, @@ -70,19 +70,19 @@ async def test_listener(response: Response): assert isinstance(expected_result, StatusData) assert isinstance(expected_response, Response) - @_clear_listener_on_rerun(Endpoint.backup()) - async def test_request_backup(self, client: Client, app: Application): - endpoint: Endpoint = Endpoint.backup() - expected_result: Backup | None - expected_response: Backup | None = None + @_clear_listener_on_rerun(Endpoint.snapshot()) + async def test_request_snapshot(self, client: Client, app: Application): + endpoint: Endpoint = Endpoint.snapshot() + expected_result: Snapshot | None + expected_response: Snapshot | None = None @client.on_request(endpoint) async def test_listener(response: Response): nonlocal expected_response expected_response = response - expected_result = await client.backup(app.id) - assert isinstance(expected_result, Backup) + expected_result = await client.snapshot(app.id) + assert isinstance(expected_result, Snapshot) assert isinstance(expected_response, Response) @_clear_listener_on_rerun(Endpoint.start()) @@ -297,10 +297,10 @@ async def test_listener(response: Response): assert isinstance(expected_result, str) assert isinstance(expected_response, Response) - @_clear_listener_on_rerun(Endpoint.all_backups()) - async def test_all_backups(self, client: Client, app: Application): - endpoint: Endpoint = Endpoint.all_backups() - expected_result: list[BackupInfo] | None + @_clear_listener_on_rerun(Endpoint.all_snapshots()) + async def test_all_snapshots(self, client: Client, app: Application): + endpoint: Endpoint = Endpoint.all_snapshots() + expected_result: list[SnapshotInfo] | None expected_response: Response | None = None @client.on_request(endpoint) @@ -308,9 +308,9 @@ async def test_listener(response: Response): nonlocal expected_response expected_response = response - expected_result = await client.all_app_backups(app.id) + expected_result = await client.all_app_snapshots(app.id) assert isinstance(expected_result, list) - assert isinstance(expected_result[0], BackupInfo) + assert isinstance(expected_result[0], SnapshotInfo) assert isinstance(expected_response, Response) @_clear_listener_on_rerun(Endpoint.all_apps_status()) From 051fe134b110beef5addcd410af8107285f75807 Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Sun, 28 Sep 2025 22:43:57 -0300 Subject: [PATCH 5/8] test:add upload helper with rate limit to avoid hitting API limits --- tests/test_upload_app.py | 62 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/tests/test_upload_app.py b/tests/test_upload_app.py index 1357024..e85bf64 100644 --- a/tests/test_upload_app.py +++ b/tests/test_upload_app.py @@ -1,4 +1,5 @@ import asyncio +import time import pytest @@ -8,6 +9,22 @@ from . import create_zip +_last_upload_time = 0 +UPLOAD_RATELIMIT_IN_SECONDS = 3 + +async def upload_app(client: Client, config: ConfigFile | str) -> UploadData: + global _last_upload_time + elapsed = time.time() - _last_upload_time + + if elapsed < UPLOAD_RATELIMIT_IN_SECONDS: + await asyncio.sleep(UPLOAD_RATELIMIT_IN_SECONDS - elapsed) + + _last_upload_time = time.time() + return await client.upload_app( + File(create_zip(config), filename='file.zip') + ) + + @pytest.mark.asyncio(scope='session') @pytest.mark.upload @@ -18,22 +35,18 @@ async def test_normal_upload(self, client: Client): main='main.py', memory=256, ) - await asyncio.sleep(10) - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_invalid_main_upload(self, client: Client): config = ConfigFile( display_name='invalid_main', - main='index.js', + main='invalid_main.py', memory=256, ) with pytest.raises(errors.InvalidMain): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_missing_main_upload(self, client: Client): @@ -43,11 +56,10 @@ async def test_missing_main_upload(self, client: Client): memory=256, ) with pytest.raises(errors.MissingMainFile): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) + @pytest.mark.skip async def test_few_memory_upload(self, client: Client): config = ConfigFile( display_name='few_memory_test', @@ -55,9 +67,7 @@ async def test_few_memory_upload(self, client: Client): memory=9999, ) with pytest.raises(errors.FewMemory): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_invalid_display_name_upload(self, client: Client): @@ -67,9 +77,7 @@ async def test_invalid_display_name_upload(self, client: Client): memory=256, ) with pytest.raises(errors.InvalidDisplayName): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_missing_display_name_upload(self, client: Client): @@ -79,9 +87,7 @@ async def test_missing_display_name_upload(self, client: Client): memory=256, ) with pytest.raises(errors.MissingDisplayName): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_bad_memory_upload(self, client: Client): @@ -92,9 +98,7 @@ async def test_bad_memory_upload(self, client: Client): ).content() with pytest.raises(errors.BadMemory): config = config.replace('256', '1') - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_missing_memory_upload(self, client: Client): @@ -103,9 +107,7 @@ async def test_missing_memory_upload(self, client: Client): ).content() config = config.replace('256', '') with pytest.raises(errors.MissingMemory): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_invalid_version_upload(self, client: Client): @@ -116,9 +118,7 @@ async def test_invalid_version_upload(self, client: Client): ).content() config = config.replace('recommended', 'invalid_version') with pytest.raises(errors.InvalidVersion): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) async def test_missing_version_upload(self, client: Client): @@ -129,7 +129,5 @@ async def test_missing_version_upload(self, client: Client): ).content() config = config.replace('recommended', '') with pytest.raises(errors.MissingVersion): - upload_data: UploadData = await client.upload_app( - File(create_zip(config), filename='file.zip') - ) + upload_data: UploadData = await upload_app(client, config) await client.delete_app(upload_data.id) From 5e12c186b69fcaa13a79c91448dae846763b6f9f Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Tue, 30 Sep 2025 10:56:07 -0300 Subject: [PATCH 6/8] feat: add type verification for api_key --- squarecloud/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/squarecloud/client.py b/squarecloud/client.py index 9e8e077..10fe220 100644 --- a/squarecloud/client.py +++ b/squarecloud/client.py @@ -64,6 +64,10 @@ def __init__( """ self.log_level = log_level self._api_key = api_key + + if not isinstance(self._api_key, str): + raise TypeError("api_key must be str") + self._http = HTTPClient(api_key=api_key) self.logger = logger logger.setLevel(log_level) From 1480adf03fd22c14abfd53296e07633126b5ee79 Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Tue, 30 Sep 2025 11:14:57 -0300 Subject: [PATCH 7/8] fix:add missing self._snapshot = None --- squarecloud/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/squarecloud/app.py b/squarecloud/app.py index 5caa305..e01b54e 100644 --- a/squarecloud/app.py +++ b/squarecloud/app.py @@ -128,6 +128,7 @@ def clear(self) -> None: self._logs = None self._backup = None self._app_data = None + self._snapshot = None def update(self, *args) -> None: """ From cc9e1603d215a2e41ed5119be7b9da3dae585ff1 Mon Sep 17 00:00:00 2001 From: BarbosaDe Date: Tue, 30 Sep 2025 11:56:55 -0300 Subject: [PATCH 8/8] fix(test): fix the container_already_stop error when running the test --- tests/__init__.py | 2 +- tests/test_request_listeners.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 5538f70..fab2265 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,7 +21,7 @@ def create_zip(config: ConfigFile | str): with zipfile.ZipFile(buffer, 'w') as zip_file: zip_file.writestr('requirements.txt', 'discord.py') - zip_file.writestr('main.py', "print('ok')") + zip_file.writestr('main.py', "print('ok')\nwhile True:pass") zip_file.writestr('squarecloud.app', config) diff --git a/tests/test_request_listeners.py b/tests/test_request_listeners.py index a5a537e..48780c3 100644 --- a/tests/test_request_listeners.py +++ b/tests/test_request_listeners.py @@ -85,9 +85,9 @@ async def test_listener(response: Response): assert isinstance(expected_result, Snapshot) assert isinstance(expected_response, Response) - @_clear_listener_on_rerun(Endpoint.start()) - async def test_request_start_app(self, client: Client, app: Application): - endpoint: Endpoint = Endpoint.start() + @_clear_listener_on_rerun(Endpoint.stop()) + async def test_request_stop_app(self, client: Client, app: Application): + endpoint: Endpoint = Endpoint.stop() expected_result: Response | None expected_response: Response | None = None @@ -96,13 +96,13 @@ async def test_listener(response: Response): nonlocal expected_response expected_response = response - expected_result = await client.start_app(app.id) + expected_result = await client.stop_app(app.id) assert isinstance(expected_result, Response) assert isinstance(expected_response, Response) - @_clear_listener_on_rerun(Endpoint.stop()) - async def test_request_stop_app(self, client: Client, app: Application): - endpoint: Endpoint = Endpoint.stop() + @_clear_listener_on_rerun(Endpoint.start()) + async def test_request_start_app(self, client: Client, app: Application): + endpoint: Endpoint = Endpoint.start() expected_result: Response | None expected_response: Response | None = None @@ -111,7 +111,7 @@ async def test_listener(response: Response): nonlocal expected_response expected_response = response - expected_result = await client.stop_app(app.id) + expected_result = await client.start_app(app.id) assert isinstance(expected_result, Response) assert isinstance(expected_response, Response)