add tests

This commit is contained in:
maxDorninger
2025-06-07 16:45:15 +02:00
parent cc5e9837ec
commit 5c646db42a
7 changed files with 686 additions and 1 deletions

View File

@@ -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]

View File

@@ -0,0 +1 @@

View File

@@ -28,4 +28,5 @@ dependencies = [
"fastapi-utils>=0.8.0",
"apscheduler>=3.11.0",
"alembic>=1.16.1",
"pytest>=8.4.0",
]

0
tests/__init__.py Normal file
View File

0
tests/tv/__init__.py Normal file
View File

645
tests/tv/test_service.py Normal file
View File

@@ -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 == []

36
uv.lock generated
View File

@@ -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"