From 5c646db42a28174f96acfac49525770ed3bff860 Mon Sep 17 00:00:00 2001 From: maxDorninger <97409287+maxDorninger@users.noreply.github.com> Date: Sat, 7 Jun 2025 16:45:15 +0200 Subject: [PATCH] add tests --- media_manager/indexer/models.py | 4 +- media_manager/tests/tv/test_service.py | 1 + pyproject.toml | 1 + tests/__init__.py | 0 tests/tv/__init__.py | 0 tests/tv/test_service.py | 645 +++++++++++++++++++++++++ uv.lock | 36 ++ 7 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 media_manager/tests/tv/test_service.py create mode 100644 tests/__init__.py create mode 100644 tests/tv/__init__.py create mode 100644 tests/tv/test_service.py diff --git a/media_manager/indexer/models.py b/media_manager/indexer/models.py index 9057c68..0ee319e 100644 --- a/media_manager/indexer/models.py +++ b/media_manager/indexer/models.py @@ -1,3 +1,5 @@ +from uuid import UUID + from sqlalchemy import String, Integer from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy.orm import Mapped, mapped_column @@ -9,7 +11,7 @@ from media_manager.torrent.schemas import Quality class IndexerQueryResult(Base): __tablename__ = "indexer_query_result" - id: Mapped[IndexerQueryResultId] = mapped_column(primary_key=True) + id: Mapped[UUID] = mapped_column(primary_key=True) title: Mapped[str] download_url: Mapped[str] seeders: Mapped[int] diff --git a/media_manager/tests/tv/test_service.py b/media_manager/tests/tv/test_service.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/media_manager/tests/tv/test_service.py @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml index 770b1ee..96a395b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,4 +28,5 @@ dependencies = [ "fastapi-utils>=0.8.0", "apscheduler>=3.11.0", "alembic>=1.16.1", + "pytest>=8.4.0", ] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tv/__init__.py b/tests/tv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tv/test_service.py b/tests/tv/test_service.py new file mode 100644 index 0000000..5f4c5c9 --- /dev/null +++ b/tests/tv/test_service.py @@ -0,0 +1,645 @@ +import uuid +from unittest.mock import MagicMock, patch + +import pytest + +from media_manager.tv.exceptions import NotFoundError +from media_manager.tv.schemas import Show, ShowId, SeasonId +from media_manager.tv.service import TvService +from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId +from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult +from media_manager.torrent.models import Quality + + +@pytest.fixture +def mock_tv_repository(): + return MagicMock() + + +@pytest.fixture +def tv_service(mock_tv_repository): + return TvService(tv_repository=mock_tv_repository) + + +def test_add_show(tv_service, mock_tv_repository): + external_id = 123 + metadata_provider = "tmdb" + show_data = Show( + id=ShowId(uuid.uuid4()), + name="Test Show", + overview="Test Overview", + year=2022, + external_id=external_id, + metadata_provider=metadata_provider, + seasons=[], + ) + + with patch( + "media_manager.metadataProvider.get_show_metadata", return_value=show_data + ) as mock_get_metadata: + mock_tv_repository.save_show.return_value = show_data + result = tv_service.add_show( + external_id=external_id, metadata_provider=metadata_provider + ) + + mock_get_metadata.assert_called_once_with( + id=external_id, provider=metadata_provider + ) + mock_tv_repository.save_show.assert_called_once_with(show=show_data) + assert result == show_data + + +def test_add_show_with_invalid_metadata(monkeypatch, tv_service, mock_tv_repository): + external_id = 123 + metadata_provider = "tmdb" + # Simulate metadata provider returning None + monkeypatch.setattr( + "media_manager.metadataProvider.get_show_metadata", lambda id, provider: None + ) + mock_tv_repository.save_show.return_value = None + result = tv_service.add_show( + external_id=external_id, metadata_provider=metadata_provider + ) + assert result is None + + +def test_check_if_show_exists_by_external_id(tv_service, mock_tv_repository): + external_id = 123 + metadata_provider = "tmdb" + mock_tv_repository.get_show_by_external_id.return_value = "show_obj" + assert tv_service.check_if_show_exists( + external_id=external_id, metadata_provider=metadata_provider + ) + mock_tv_repository.get_show_by_external_id.assert_called_once_with( + external_id=external_id, metadata_provider=metadata_provider + ) + + mock_tv_repository.get_show_by_external_id.side_effect = NotFoundError + assert not tv_service.check_if_show_exists( + external_id=external_id, metadata_provider=metadata_provider + ) + + +def test_check_if_show_exists_by_show_id(tv_service, mock_tv_repository): + show_id = ShowId(uuid.uuid4()) + mock_tv_repository.get_show_by_id.return_value = "show_obj" + assert tv_service.check_if_show_exists(show_id=show_id) + mock_tv_repository.get_show_by_id.assert_called_once_with(show_id=show_id) + + mock_tv_repository.get_show_by_id.side_effect = NotFoundError + assert not tv_service.check_if_show_exists(show_id=show_id) + + +def test_check_if_show_exists_with_invalid_uuid(tv_service, mock_tv_repository): + # Simulate NotFoundError for a random UUID + show_id = uuid.uuid4() + mock_tv_repository.get_show_by_id.side_effect = NotFoundError + assert not tv_service.check_if_show_exists(show_id=show_id) + + +def test_check_if_show_exists_raises_value_error(tv_service): + with pytest.raises(ValueError): + tv_service.check_if_show_exists() + + +def test_add_season_request(tv_service, mock_tv_repository): + season_request = MagicMock() + mock_tv_repository.add_season_request.return_value = season_request + result = tv_service.add_season_request(season_request) + mock_tv_repository.add_season_request.assert_called_once_with( + season_request=season_request + ) + assert result == season_request + + +def test_get_season_request_by_id(tv_service, mock_tv_repository): + season_request_id = MagicMock() + season_request = MagicMock() + mock_tv_repository.get_season_request.return_value = season_request + result = tv_service.get_season_request_by_id(season_request_id) + mock_tv_repository.get_season_request.assert_called_once_with( + season_request_id=season_request_id + ) + assert result == season_request + + +def test_update_season_request(tv_service, mock_tv_repository): + season_request = MagicMock() + mock_tv_repository.add_season_request.return_value = season_request + result = tv_service.update_season_request(season_request) + mock_tv_repository.delete_season_request.assert_called_once_with( + season_request_id=season_request.id + ) + mock_tv_repository.add_season_request.assert_called_once_with( + season_request=season_request + ) + assert result == season_request + + +def test_delete_season_request(tv_service, mock_tv_repository): + season_request_id = MagicMock() + tv_service.delete_season_request(season_request_id) + mock_tv_repository.delete_season_request.assert_called_once_with( + season_request_id=season_request_id + ) + + +def test_get_all_shows(tv_service, mock_tv_repository): + shows = [MagicMock(), MagicMock()] + mock_tv_repository.get_shows.return_value = shows + result = tv_service.get_all_shows() + mock_tv_repository.get_shows.assert_called_once() + assert result == shows + + +def test_get_show_by_id(tv_service, mock_tv_repository): + show_id = MagicMock() + show = MagicMock() + mock_tv_repository.get_show_by_id.return_value = show + result = tv_service.get_show_by_id(show_id) + mock_tv_repository.get_show_by_id.assert_called_once_with(show_id=show_id) + assert result == show + + +def test_get_show_by_id_not_found(tv_service, mock_tv_repository): + show_id = uuid.uuid4() + mock_tv_repository.get_show_by_id.side_effect = NotFoundError + try: + tv_service.get_show_by_id(show_id) + except NotFoundError: + assert True + else: + assert False + + +def test_get_show_by_external_id(tv_service, mock_tv_repository): + external_id = 123 + metadata_provider = "tmdb" + show = MagicMock() + mock_tv_repository.get_show_by_external_id.return_value = show + result = tv_service.get_show_by_external_id(external_id, metadata_provider) + mock_tv_repository.get_show_by_external_id.assert_called_once_with( + external_id=external_id, metadata_provider=metadata_provider + ) + assert result == show + + +def test_get_show_by_external_id_not_found(tv_service, mock_tv_repository): + external_id = 123 + metadata_provider = "tmdb" + mock_tv_repository.get_show_by_external_id.side_effect = NotFoundError + try: + tv_service.get_show_by_external_id(external_id, metadata_provider) + except NotFoundError: + assert True + else: + assert False + + +def test_get_season(tv_service, mock_tv_repository): + season_id = MagicMock() + season = MagicMock() + mock_tv_repository.get_season.return_value = season + result = tv_service.get_season(season_id) + mock_tv_repository.get_season.assert_called_once_with(season_id=season_id) + assert result == season + + +def test_get_season_not_found(tv_service, mock_tv_repository): + season_id = uuid.uuid4() + mock_tv_repository.get_season.side_effect = NotFoundError + try: + tv_service.get_season(season_id) + except NotFoundError: + assert True + else: + assert False + + +def test_get_all_season_requests(tv_service, mock_tv_repository): + requests = [MagicMock(), MagicMock()] + mock_tv_repository.get_season_requests.return_value = requests + result = tv_service.get_all_season_requests() + mock_tv_repository.get_season_requests.assert_called_once() + assert result == requests + + +def test_get_public_season_files_by_season_id_downloaded( + monkeypatch, tv_service, mock_tv_repository +): + season_id = MagicMock() + season_file = MagicMock() + public_season_file = MagicMock() + public_season_file.downloaded = False + mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] + monkeypatch.setattr( + "media_manager.tv.schemas.PublicSeasonFile.model_validate", + lambda x: public_season_file, + ) + monkeypatch.setattr( + tv_service, "season_file_exists_on_file", lambda season_file: True + ) + result = tv_service.get_public_season_files_by_season_id(season_id) + assert result[0].downloaded is True + + +def test_get_public_season_files_by_season_id_not_downloaded( + monkeypatch, tv_service, mock_tv_repository +): + season_id = MagicMock() + season_file = MagicMock() + public_season_file = MagicMock() + public_season_file.downloaded = False + mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] + monkeypatch.setattr( + "media_manager.tv.schemas.PublicSeasonFile.model_validate", + lambda x: public_season_file, + ) + monkeypatch.setattr( + tv_service, "season_file_exists_on_file", lambda season_file: False + ) + result = tv_service.get_public_season_files_by_season_id(season_id) + assert result[0].downloaded is False + + +def test_get_public_season_files_by_season_id_empty(tv_service, mock_tv_repository): + season_id = uuid.uuid4() + mock_tv_repository.get_season_files_by_season_id.return_value = [] + result = tv_service.get_public_season_files_by_season_id(season_id) + assert result == [] + + +def test_is_season_downloaded_true(monkeypatch, tv_service, mock_tv_repository): + season_id = MagicMock() + season_file = MagicMock() + mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] + monkeypatch.setattr( + tv_service, "season_file_exists_on_file", lambda season_file: True + ) + assert tv_service.is_season_downloaded(season_id) is True + + +def test_is_season_downloaded_false(monkeypatch, tv_service, mock_tv_repository): + season_id = MagicMock() + season_file = MagicMock() + mock_tv_repository.get_season_files_by_season_id.return_value = [season_file] + monkeypatch.setattr( + tv_service, "season_file_exists_on_file", lambda season_file: False + ) + assert tv_service.is_season_downloaded(season_id) is False + + +def test_is_season_downloaded_with_no_files(tv_service, mock_tv_repository): + season_id = uuid.uuid4() + mock_tv_repository.get_season_files_by_season_id.return_value = [] + assert tv_service.is_season_downloaded(season_id) is False + + +def test_season_file_exists_on_file_none(monkeypatch, tv_service): + season_file = MagicMock() + season_file.torrent_id = None + assert tv_service.season_file_exists_on_file(season_file) is True + + +def test_season_file_exists_on_file_imported(monkeypatch, tv_service): + season_file = MagicMock() + season_file.torrent_id = "torrent_id" + torrent_file = MagicMock(imported=True) + monkeypatch.setattr( + "media_manager.torrent.repository.get_torrent_by_id", + lambda db, torrent_id: torrent_file, + ) + tv_service.tv_repository.db = MagicMock() + assert tv_service.season_file_exists_on_file(season_file) is True + + +def test_season_file_exists_on_file_not_imported(monkeypatch, tv_service): + season_file = MagicMock() + season_file.torrent_id = "torrent_id" + torrent_file = MagicMock(imported=False) + monkeypatch.setattr( + "media_manager.torrent.repository.get_torrent_by_id", + lambda db, torrent_id: torrent_file, + ) + tv_service.tv_repository.db = MagicMock() + assert tv_service.season_file_exists_on_file(season_file) is False + + +def test_season_file_exists_on_file_with_none_imported(monkeypatch, tv_service): + class DummyFile: + def __init__(self): + self.torrent_id = uuid.uuid4() + + dummy_file = DummyFile() + + # Simulate a torrent object with imported=True + class DummyTorrent: + imported = True + + monkeypatch.setattr( + "media_manager.torrent.repository.get_torrent_by_id", + lambda db, torrent_id: DummyTorrent(), + ) + tv_service.tv_repository.db = MagicMock() + assert tv_service.season_file_exists_on_file(dummy_file) is True + + +def test_season_file_exists_on_file_with_none_not_imported(monkeypatch, tv_service): + class DummyFile: + def __init__(self): + self.torrent_id = uuid.uuid4() + + dummy_file = DummyFile() + + # Simulate a torrent object with imported=False + class DummyTorrent: + imported = False + + monkeypatch.setattr( + "media_manager.torrent.repository.get_torrent_by_id", + lambda db, torrent_id: DummyTorrent(), + ) + tv_service.tv_repository.db = MagicMock() + assert tv_service.season_file_exists_on_file(dummy_file) is False + + +def test_get_all_available_torrents_for_a_season_no_override( + tv_service, mock_tv_repository, monkeypatch +): + show_id = ShowId(uuid.uuid4()) + season_number = 1 + show_name = "Test Show" + mock_show = Show( + id=show_id, + name=show_name, + overview="", + year=2020, + external_id=1, + metadata_provider="tmdb", + seasons=[], + ) + mock_tv_repository.get_show_by_id.return_value = mock_show + + torrent1 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Test Show 1080p S01", + download_url="url1", + seeders=10, + flags=[], + size=100, + ) + torrent2 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Test Show 720p S01", + download_url="url2", + seeders=5, + flags=[], + size=100, + ) + torrent3 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Test Show 720p S01", + download_url="url3", + seeders=20, + flags=[], + size=100, + ) + torrent4 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Test Show S01E02", + download_url="url4", + seeders=5, + flags=[], + size=100, + ) # Episode + torrent5 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Test Show S02", + download_url="url5", + seeders=10, + flags=[], + size=100, + ) # Different season + + mock_search = MagicMock( + return_value=[torrent1, torrent2, torrent3, torrent4, torrent5] + ) + monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + + results = tv_service.get_all_available_torrents_for_a_season( + season_number=season_number, show_id=show_id + ) + + mock_tv_repository.get_show_by_id.assert_called_once_with(show_id=show_id) + mock_search.assert_called_once_with( + query=f"{show_name} s{str(season_number).zfill(2)}", db=mock_tv_repository.db + ) + assert len(results) == 3 + assert torrent1 in results + assert torrent2 in results + assert torrent3 in results + assert torrent4 not in results # Should be filtered out + assert torrent5 not in results # Should be filtered out + assert results == sorted( + [torrent1, torrent3, torrent2] + ) # Test sorting according to seeders and quality + + +def test_get_all_available_torrents_for_a_season_with_override( + tv_service, mock_tv_repository, monkeypatch +): + show_id = ShowId(uuid.uuid4()) + season_number = 1 + override_query = "Custom Query S01" + mock_show = Show( + id=show_id, + name="Test Show", + overview="", + year=2020, + external_id=1, + metadata_provider="tmdb", + seasons=[], + ) + mock_tv_repository.get_show_by_id.return_value = mock_show + + torrent1 = IndexerQueryResult( + id=IndexerQueryResultId(uuid.uuid4()), + title="Custom Query S01E01", + download_url="url1", + seeders=10, + flags=[], + size=100, + season=[1], + ) + mock_search = MagicMock(return_value=[torrent1]) + monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + + results = tv_service.get_all_available_torrents_for_a_season( + season_number=season_number, + show_id=show_id, + search_query_override=override_query, + ) + + mock_search.assert_called_once_with(query=override_query, db=mock_tv_repository.db) + assert results == [torrent1] + + +def test_get_all_available_torrents_for_a_season_no_results( + tv_service, mock_tv_repository, monkeypatch +): + show_id = ShowId(uuid.uuid4()) + season_number = 1 + mock_show = Show( + id=show_id, + name="Test Show", + overview="", + year=2020, + external_id=1, + metadata_provider="tmdb", + seasons=[], + ) + mock_tv_repository.get_show_by_id.return_value = mock_show + + mock_search = MagicMock(return_value=[]) + monkeypatch.setattr("media_manager.indexer.service.search", mock_search) + + results = tv_service.get_all_available_torrents_for_a_season( + season_number=season_number, show_id=show_id + ) + assert results == [] + + +def test_search_for_show_no_existing(tv_service, monkeypatch): + query = "Test Show" + metadata_provider = "tmdb" + search_result_item = MetaDataProviderShowSearchResult( + external_id=123, + name="Test Show", + year=2022, + overview="Overview", + metadata_provider=metadata_provider, + added=False, + poster_path=None, # Added + ) + + mock_search_show = MagicMock(return_value=[search_result_item]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + + # Mock check_if_show_exists to always return False (show not added) + monkeypatch.setattr( + tv_service, "check_if_show_exists", lambda external_id, metadata_provider: False + ) + + results = tv_service.search_for_show( + query=query, metadata_provider=metadata_provider + ) + + mock_search_show.assert_called_once_with(query, metadata_provider) + assert len(results) == 1 + assert results[0] == search_result_item + assert results[0].added is False # Should not be marked as added + + +def test_search_for_show_with_existing(tv_service, monkeypatch): + query = "Test Show" + metadata_provider = "tmdb" + search_result_item = MetaDataProviderShowSearchResult( + external_id=123, + name="Test Show", + year=2022, + overview="Overview", + metadata_provider=metadata_provider, + added=False, # Initialized to False, logic will set it to True + poster_path=None, # Added + ) + + mock_search_show = MagicMock(return_value=[search_result_item]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + + # Mock check_if_show_exists to always return True (show already added) + monkeypatch.setattr( + tv_service, "check_if_show_exists", lambda external_id, metadata_provider: True + ) + + results = tv_service.search_for_show( + query=query, metadata_provider=metadata_provider + ) + + assert len(results) == 1 + assert results[0].added is True # Should be marked as added + + +def test_search_for_show_empty_results(tv_service, monkeypatch): + query = "NonExistent Show" + metadata_provider = "tmdb" + mock_search_show = MagicMock(return_value=[]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + + results = tv_service.search_for_show( + query=query, metadata_provider=metadata_provider + ) + assert results == [] + + +def test_get_popular_shows_none_added(tv_service, monkeypatch): + metadata_provider = "tmdb" + popular_show1 = MetaDataProviderShowSearchResult( + external_id=123, + name="Popular Show 1", + year=2022, + overview="Overview1", + metadata_provider=metadata_provider, + added=False, + poster_path=None, # Added + ) + popular_show2 = MetaDataProviderShowSearchResult( + external_id=456, + name="Popular Show 2", + year=2023, + overview="Overview2", + metadata_provider=metadata_provider, + added=False, + poster_path=None, # Added + ) + + mock_search_show = MagicMock(return_value=[popular_show1, popular_show2]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + monkeypatch.setattr( + tv_service, "check_if_show_exists", lambda external_id, metadata_provider: False + ) + + results = tv_service.get_popular_shows(metadata_provider=metadata_provider) + assert len(results) == 2 + assert popular_show1 in results + assert popular_show2 in results + + +def test_get_popular_shows_all_added(tv_service, monkeypatch): + metadata_provider = "tmdb" + popular_show1 = MetaDataProviderShowSearchResult( + external_id=123, + name="Popular Show 1", + year=2022, + overview="Overview1", + metadata_provider=metadata_provider, + added=False, + poster_path=None, # Added + ) + mock_search_show = MagicMock(return_value=[popular_show1]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + monkeypatch.setattr( + tv_service, "check_if_show_exists", lambda external_id, metadata_provider: True + ) + + results = tv_service.get_popular_shows(metadata_provider=metadata_provider) + assert results == [] + + +def test_get_popular_shows_empty_from_provider(tv_service, monkeypatch): + metadata_provider = "tmdb" + mock_search_show = MagicMock(return_value=[]) + monkeypatch.setattr("media_manager.metadataProvider.search_show", mock_search_show) + + results = tv_service.get_popular_shows(metadata_provider=metadata_provider) + assert results == [] + diff --git a/uv.lock b/uv.lock index 31e1c7f..59003cc 100644 --- a/uv.lock +++ b/uv.lock @@ -499,6 +499,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -628,6 +637,7 @@ dependencies = [ { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "pytest" }, { name = "python-json-logger" }, { name = "qbittorrent-api" }, { name = "requests" }, @@ -656,6 +666,7 @@ requires-dist = [ { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "pydantic", specifier = ">=2.11.5" }, { name = "pydantic-settings", specifier = ">=2.9.1" }, + { name = "pytest", specifier = ">=8.4.0" }, { name = "python-json-logger", specifier = ">=3.3.0" }, { name = "qbittorrent-api", specifier = ">=2025.5.0" }, { name = "requests", specifier = ">=2.32.3" }, @@ -694,6 +705,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/07/a7aefd5b3ee565b4d959bcf7061666c7fbf66ed83e58d07cdcdca35c9b33/patool-4.0.1-py2.py3-none-any.whl", hash = "sha256:a7430eb08edcbd71feaf9c40f55c46f6a0ac385dc68dd0f5010cfa4ad2e9341a", size = 86512, upload-time = "2025-05-02T19:08:19.407Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "psutil" version = "5.9.8" @@ -849,6 +869,22 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pytest" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.0"