diff --git a/advanced_alchemy/utils/fixtures.py b/advanced_alchemy/utils/fixtures.py index 7f833d050..8496651f6 100644 --- a/advanced_alchemy/utils/fixtures.py +++ b/advanced_alchemy/utils/fixtures.py @@ -1,4 +1,6 @@ +import csv import gzip +import io import zipfile from functools import partial from typing import TYPE_CHECKING, Any, Union @@ -15,12 +17,16 @@ def open_fixture(fixtures_path: "Union[Path, AsyncPath]", fixture_name: str) -> Any: - """Loads JSON file with the specified fixture name. + """Loads JSON or CSV file with the specified fixture name. - Supports plain JSON files, gzipped JSON files (.json.gz), and zipped JSON files (.json.zip). + Supports plain files, gzipped files (.json.gz, .csv.gz), and zipped files (.json.zip, .csv.zip). The function automatically detects the file format based on file extension and handles - decompression transparently. Supports both lowercase and uppercase variations for better - compatibility with database exports. + decompression transparently. JSON files take priority over CSV files. Supports both + lowercase and uppercase variations for better compatibility with database exports. + + For CSV files, returns a list of dictionaries using csv.DictReader where each row + becomes a dictionary with column headers as keys. Note that CSV values are always + strings, unlike JSON which preserves data types. Args: fixtures_path: The path to look for fixtures. Can be a :class:`pathlib.Path` or @@ -30,21 +36,30 @@ def open_fixture(fixtures_path: "Union[Path, AsyncPath]", fixture_name: str) -> Raises: FileNotFoundError: If no fixture file is found with any supported extension. OSError: If there's an error reading or decompressing the file. - ValueError: If the JSON content is invalid. + ValueError: If the JSON content is invalid, or if a zip file doesn't contain + the expected JSON/CSV files. zipfile.BadZipFile: If the zip file is corrupted. gzip.BadGzipFile: If the gzip file is corrupted. + csv.Error: If the CSV content is malformed. Returns: - Any: The parsed JSON data from the fixture file. + Any: The parsed data from the fixture file (JSON data or list of dictionaries from CSV). Examples: >>> from pathlib import Path >>> fixtures_path = Path("./fixtures") - >>> data = open_fixture( - ... fixtures_path, "users" - ... ) # loads users.json, users.json.gz, or users.json.zip + + # Load JSON fixture + >>> data = open_fixture(fixtures_path, "users") + # loads users.json, users.json.gz, or users.json.zip >>> print(data) [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + + # Load CSV fixture + >>> data = open_fixture(fixtures_path, "states") + # loads states.csv, states.csv.gz, or states.csv.zip + >>> print(data) + [{"abbreviation": "AL", "name": "Alabama"}, {"abbreviation": "AK", "name": "Alaska"}] """ from pathlib import Path @@ -52,26 +67,35 @@ def open_fixture(fixtures_path: "Union[Path, AsyncPath]", fixture_name: str) -> # Try different file extensions in order of preference # Include both case variations for better compatibility with database exports + # JSON formats take priority over CSV for backward compatibility file_variants = [ - (base_path / f"{fixture_name}.json", "plain"), - (base_path / f"{fixture_name.upper()}.json.gz", "gzip"), # Uppercase first (common for exports) - (base_path / f"{fixture_name}.json.gz", "gzip"), - (base_path / f"{fixture_name.upper()}.json.zip", "zip"), - (base_path / f"{fixture_name}.json.zip", "zip"), + (base_path / f"{fixture_name}.json", "json_plain"), + (base_path / f"{fixture_name.upper()}.json.gz", "json_gzip"), # Uppercase first (common for exports) + (base_path / f"{fixture_name}.json.gz", "json_gzip"), + (base_path / f"{fixture_name.upper()}.json.zip", "json_zip"), + (base_path / f"{fixture_name}.json.zip", "json_zip"), + (base_path / f"{fixture_name}.csv", "csv_plain"), + (base_path / f"{fixture_name.upper()}.csv", "csv_plain"), # Uppercase plain CSV + (base_path / f"{fixture_name.upper()}.csv.gz", "csv_gzip"), + (base_path / f"{fixture_name}.csv.gz", "csv_gzip"), + (base_path / f"{fixture_name.upper()}.csv.zip", "csv_zip"), + (base_path / f"{fixture_name}.csv.zip", "csv_zip"), ] for fixture_path, file_type in file_variants: if fixture_path.exists(): try: - f_data: str - if file_type == "plain": + # JSON handling + if file_type == "json_plain": with fixture_path.open(mode="r", encoding="utf-8") as f: f_data = f.read() - elif file_type == "gzip": + return decode_json(f_data) + if file_type == "json_gzip": with fixture_path.open(mode="rb") as f: compressed_data = f.read() f_data = gzip.decompress(compressed_data).decode("utf-8") - elif file_type == "zip": + return decode_json(f_data) + if file_type == "json_zip": with zipfile.ZipFile(fixture_path, mode="r") as zf: # Look for JSON file inside zip json_files = [name for name in zf.namelist() if name.endswith(".json")] @@ -84,27 +108,92 @@ def open_fixture(fixtures_path: "Union[Path, AsyncPath]", fixture_name: str) -> with zf.open(json_file, mode="r") as f: f_data = f.read().decode("utf-8") - else: - continue # Skip unknown file types + return decode_json(f_data) + + # CSV handling + if file_type == "csv_plain": + with fixture_path.open(mode="r", encoding="utf-8-sig", newline="") as f: + reader = csv.DictReader(f) + return list(reader) + if file_type == "csv_gzip": + with fixture_path.open(mode="rb") as f: + compressed_data = f.read() + f_data = gzip.decompress(compressed_data).decode("utf-8-sig") + reader = csv.DictReader(io.StringIO(f_data)) + return list(reader) + if file_type == "csv_zip": + with zipfile.ZipFile(fixture_path, mode="r") as zf: + # Look for CSV file inside zip + csv_files = [name for name in zf.namelist() if name.endswith(".csv")] + if not csv_files: + msg = f"No CSV files found in zip archive: {fixture_path}" + raise ValueError(msg) - return decode_json(f_data) + # Use the first CSV file found, or prefer one matching the fixture name + csv_file = next((name for name in csv_files if name == f"{fixture_name}.csv"), csv_files[0]) + + with zf.open(csv_file, mode="r") as f: + f_data = f.read().decode("utf-8-sig") + reader = csv.DictReader(io.StringIO(f_data)) + return list(reader) + continue # Skip unknown file types except (OSError, zipfile.BadZipFile, gzip.BadGzipFile) as exc: msg = f"Error reading fixture file {fixture_path}: {exc}" raise OSError(msg) from exc # No valid fixture file found - msg = f"Could not find the {fixture_name} fixture (tried .json, .json.gz, .json.zip with case variations)" + msg = f"Could not find the {fixture_name} fixture (tried .json, .json.gz, .json.zip, .csv, .csv.gz, .csv.zip with case variations)" raise FileNotFoundError(msg) +def _read_zip_file(path: "AsyncPath", name: str) -> str: + """Helper function to read JSON zip files.""" + with zipfile.ZipFile(str(path), mode="r") as zf: + # Look for JSON file inside zip + json_files = [file for file in zf.namelist() if file.endswith(".json")] + if not json_files: + error_msg = f"No JSON files found in zip archive: {path}" + raise ValueError(error_msg) + + # Use the first JSON file found, or prefer one matching the fixture name + json_file = next((file for file in json_files if file == f"{name}.json"), json_files[0]) + + with zf.open(json_file, mode="r") as f: + return f.read().decode("utf-8") + + +def _read_csv_zip_file(path: "AsyncPath", name: str) -> "list[dict[str, Any]]": + """Helper function to read CSV zip files.""" + with zipfile.ZipFile(str(path), mode="r") as zf: + # Look for CSV file inside zip + csv_files = [file for file in zf.namelist() if file.endswith(".csv")] + if not csv_files: + error_msg = f"No CSV files found in zip archive: {path}" + raise ValueError(error_msg) + + # Use the first CSV file found, or prefer one matching the fixture name + csv_file = next((file for file in csv_files if file == f"{name}.csv"), csv_files[0]) + + with zf.open(csv_file, mode="r") as f: + f_data = f.read().decode("utf-8-sig") + + reader = csv.DictReader(io.StringIO(f_data)) + return list(reader) + + async def open_fixture_async(fixtures_path: "Union[Path, AsyncPath]", fixture_name: str) -> Any: - """Loads JSON file with the specified fixture name asynchronously. + """Loads JSON or CSV file with the specified fixture name asynchronously. - Supports plain JSON files, gzipped JSON files (.json.gz), and zipped JSON files (.json.zip). + Supports plain files, gzipped files (.json.gz, .csv.gz), and zipped files (.json.zip, .csv.zip). The function automatically detects the file format based on file extension and handles - decompression transparently. Supports both lowercase and uppercase variations for better - compatibility with database exports. For compressed files, decompression is performed - synchronously in a thread pool to avoid blocking the event loop. + decompression transparently. JSON files take priority over CSV files. Supports both + lowercase and uppercase variations for better compatibility with database exports. + For compressed files and CSV parsing, operations are performed in a thread pool to + avoid blocking the event loop. + + For CSV files, returns a list of dictionaries using csv.DictReader where each row + becomes a dictionary with column headers as keys. Note that CSV values are always + strings, unlike JSON which preserves data types. Args: fixtures_path: The path to look for fixtures. Can be a :class:`pathlib.Path` or @@ -115,21 +204,30 @@ async def open_fixture_async(fixtures_path: "Union[Path, AsyncPath]", fixture_na MissingDependencyError: If the `anyio` library is not installed. FileNotFoundError: If no fixture file is found with any supported extension. OSError: If there's an error reading or decompressing the file. - ValueError: If the JSON content is invalid. + ValueError: If the JSON content is invalid, or if a zip file doesn't contain + the expected JSON/CSV files. zipfile.BadZipFile: If the zip file is corrupted. gzip.BadGzipFile: If the gzip file is corrupted. + csv.Error: If the CSV content is malformed. Returns: - Any: The parsed JSON data from the fixture file. + Any: The parsed data from the fixture file (JSON data or list of dictionaries from CSV). Examples: >>> from anyio import Path as AsyncPath >>> fixtures_path = AsyncPath("./fixtures") - >>> data = await open_fixture_async( - ... fixtures_path, "users" - ... ) # loads users.json, users.json.gz, or users.json.zip + + # Load JSON fixture + >>> data = await open_fixture_async(fixtures_path, "users") + # loads users.json, users.json.gz, or users.json.zip >>> print(data) [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}] + + # Load CSV fixture + >>> data = await open_fixture_async(fixtures_path, "states") + # loads states.csv, states.csv.gz, or states.csv.zip + >>> print(data) + [{"abbreviation": "AL", "name": "Alabama"}, {"abbreviation": "AK", "name": "Alaska"}] """ try: from anyio import Path as AsyncPath @@ -139,61 +237,76 @@ async def open_fixture_async(fixtures_path: "Union[Path, AsyncPath]", fixture_na from advanced_alchemy.utils.sync_tools import async_ - def _read_zip_file(path: "AsyncPath", name: str) -> str: - """Helper function to read zip files.""" - with zipfile.ZipFile(str(path), mode="r") as zf: - # Look for JSON file inside zip - json_files = [file for file in zf.namelist() if file.endswith(".json")] - if not json_files: - error_msg = f"No JSON files found in zip archive: {path}" - raise ValueError(error_msg) - - # Use the first JSON file found, or prefer one matching the fixture name - json_file = next((file for file in json_files if file == f"{name}.json"), json_files[0]) - - with zf.open(json_file, mode="r") as f: - return f.read().decode("utf-8") - base_path = AsyncPath(fixtures_path) # Try different file extensions in order of preference # Include both case variations for better compatibility with database exports + # JSON formats take priority over CSV for backward compatibility file_variants = [ - (base_path / f"{fixture_name}.json", "plain"), - (base_path / f"{fixture_name.upper()}.json.gz", "gzip"), # Uppercase first (common for exports) - (base_path / f"{fixture_name}.json.gz", "gzip"), - (base_path / f"{fixture_name.upper()}.json.zip", "zip"), - (base_path / f"{fixture_name}.json.zip", "zip"), + (base_path / f"{fixture_name}.json", "json_plain"), + (base_path / f"{fixture_name.upper()}.json.gz", "json_gzip"), # Uppercase first (common for exports) + (base_path / f"{fixture_name}.json.gz", "json_gzip"), + (base_path / f"{fixture_name.upper()}.json.zip", "json_zip"), + (base_path / f"{fixture_name}.json.zip", "json_zip"), + (base_path / f"{fixture_name}.csv", "csv_plain"), + (base_path / f"{fixture_name.upper()}.csv", "csv_plain"), # Uppercase plain CSV + (base_path / f"{fixture_name.upper()}.csv.gz", "csv_gzip"), + (base_path / f"{fixture_name}.csv.gz", "csv_gzip"), + (base_path / f"{fixture_name.upper()}.csv.zip", "csv_zip"), + (base_path / f"{fixture_name}.csv.zip", "csv_zip"), ] for fixture_path, file_type in file_variants: if await fixture_path.exists(): try: - f_data: str - if file_type == "plain": + # JSON handling + if file_type == "json_plain": async with await fixture_path.open(mode="r", encoding="utf-8") as f: f_data = await f.read() - elif file_type == "gzip": + return decode_json(f_data) + if file_type == "json_gzip": # Read gzipped files using binary pattern async with await fixture_path.open(mode="rb") as f: # type: ignore[assignment] - compressed_data: bytes = await f.read() # type: ignore[assignment] + compressed_json: bytes = await f.read() # type: ignore[assignment] # Decompress in thread pool to avoid blocking def _decompress_gzip(data: bytes) -> str: return gzip.decompress(data).decode("utf-8") - f_data = await async_(partial(_decompress_gzip, compressed_data))() - elif file_type == "zip": + f_data = await async_(partial(_decompress_gzip, compressed_json))() + return decode_json(f_data) + if file_type == "json_zip": # Read zipped files in thread pool to avoid blocking f_data = await async_(partial(_read_zip_file, fixture_path, fixture_name))() - else: - continue # Skip unknown file types + return decode_json(f_data) + + # CSV handling + if file_type == "csv_plain": + async with await fixture_path.open(mode="r", encoding="utf-8-sig") as f: + f_data = await f.read() + + # Parse CSV in thread pool to avoid blocking + def _parse_csv(data: str) -> "list[dict[str, Any]]": + reader = csv.DictReader(io.StringIO(data)) + return list(reader) + + return await async_(partial(_parse_csv, f_data))() + if file_type == "csv_gzip": + async with await fixture_path.open(mode="rb") as f: # type: ignore[assignment] + compressed_csv: bytes = await f.read() # type: ignore[assignment] + + def _decompress_and_parse_csv(data: bytes) -> "list[dict[str, Any]]": + decompressed = gzip.decompress(data).decode("utf-8-sig") + reader = csv.DictReader(io.StringIO(decompressed)) + return list(reader) - return decode_json(f_data) + return await async_(partial(_decompress_and_parse_csv, compressed_csv))() + if file_type == "csv_zip": + return await async_(partial(_read_csv_zip_file, fixture_path, fixture_name))() except (OSError, zipfile.BadZipFile, gzip.BadGzipFile) as exc: msg = f"Error reading fixture file {fixture_path}: {exc}" raise OSError(msg) from exc # No valid fixture file found - msg = f"Could not find the {fixture_name} fixture (tried .json, .json.gz, .json.zip with case variations)" + msg = f"Could not find the {fixture_name} fixture (tried .json, .json.gz, .json.zip, .csv, .csv.gz, .csv.zip with case variations)" raise FileNotFoundError(msg) diff --git a/docs/usage/database_seeding.rst b/docs/usage/database_seeding.rst index cdd8bc7fe..d988c5fcc 100644 --- a/docs/usage/database_seeding.rst +++ b/docs/usage/database_seeding.rst @@ -2,12 +2,12 @@ Database Seeding and Fixture Loading ==================================== -Advanced Alchemy provides utilities for seeding your database with initial data through JSON fixtures. This documentation will show you how to create and load fixtures in both synchronous and asynchronous applications. +Advanced Alchemy provides utilities for seeding your database with initial data through JSON or CSV fixtures. This documentation will show you how to create and load fixtures in both synchronous and asynchronous applications. Creating Fixtures ----------------- -Fixtures in Advanced Alchemy are simple JSON files that contain the data you want to seed. Each fixture file should: +Fixtures in Advanced Alchemy are JSON or CSV files that contain the data you want to seed. Each fixture file should: 1. Contain a JSON object or a JSON array of objects, where each object represents a row in your database table 2. Include all required fields for your model @@ -506,6 +506,7 @@ Tips for Efficient Seeding - Use ``upsert_many()`` to update your data if you are updating prices for example. - You can use the database seeding from your cli, app startup or any route. - For large datasets, consider chunking the data into smaller batches. -- **Compressed Fixtures**: Large fixture files can be automatically compressed using gzip or zip formats. The system will automatically detect and decompress ``.gz`` and ``.zip`` files when loading fixtures, making it easier to manage large datasets while reducing storage space. +- **Compressed Fixtures**: Large fixture files can be automatically compressed using gzip or zip formats. The system will automatically detect and decompress ``.json.gz``, ``.json.zip``, ``.csv.gz``, and ``.csv.zip`` files when loading fixtures, making it easier to manage large datasets while reducing storage space. +- **CSV Fixtures**: For tabular data, CSV files can be used instead of JSON. CSV fixtures return a list of dictionaries where each row becomes a dictionary with column headers as keys. Note that CSV values are always strings, unlike JSON which preserves data types. JSON fixtures take priority over CSV when both exist with the same name. - When dealing with relationships, seed parent records before child records. - Consider using factory libraries like `Polyfactory `__ for generating test data. diff --git a/pyproject.toml b/pyproject.toml index 3bc7a3061..c4e234d49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -471,6 +471,11 @@ follow_imports = "skip" ignore_missing_imports = true module = ["pytest", "pytest.*", "_pytest", "_pytest.*"] +[[tool.mypy.overrides]] +follow_imports = "skip" +ignore_missing_imports = true +module = ["sphinx", "sphinx.*"] + [[tool.mypy.overrides]] module = "advanced_alchemy._serialization" warn_unused_ignores = false diff --git a/tests/unit/test_utils/test_fixtures.py b/tests/unit/test_utils/test_fixtures.py index fca1fd798..fa77909f5 100644 --- a/tests/unit/test_utils/test_fixtures.py +++ b/tests/unit/test_utils/test_fixtures.py @@ -1,6 +1,8 @@ """Tests for advanced_alchemy.utils.fixtures module.""" +import csv import gzip +import io import json import tempfile import zipfile @@ -24,7 +26,19 @@ def sample_data() -> "list[dict[str, Any]]": @pytest.fixture -def temp_fixtures_dir(sample_data: "list[dict[str, Any]]") -> "Generator[Path, None, None]": +def sample_csv_data() -> "list[dict[str, str]]": + """Sample CSV data for testing (note: CSV values are strings).""" + return [ + {"id": "1", "name": "Alice", "email": "alice@example.com"}, + {"id": "2", "name": "Bob", "email": "bob@example.com"}, + {"id": "3", "name": "Charlie", "email": "charlie@example.com"}, + ] + + +@pytest.fixture +def temp_fixtures_dir( + sample_data: "list[dict[str, Any]]", sample_csv_data: "list[dict[str, str]]" +) -> "Generator[Path, None, None]": """Create temporary directory with test fixtures in various formats.""" with tempfile.TemporaryDirectory() as temp_dir: fixtures_path = Path(temp_dir) @@ -66,6 +80,57 @@ def temp_fixtures_dir(sample_data: "list[dict[str, Any]]") -> "Generator[Path, N with zipfile.ZipFile(no_json_zip_file, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr("readme.txt", "No JSON files here") + # Create plain CSV fixture + csv_file = fixtures_path / "users_csv.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + if sample_csv_data: + writer = csv.DictWriter(f, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + + # Create gzipped CSV fixture + gz_csv_file = fixtures_path / "users_csv_gz.csv.gz" + with gzip.open(gz_csv_file, "wt", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + + # Create zipped CSV fixture (single file) + zip_csv_file = fixtures_path / "users_csv_zip.csv.zip" + with zipfile.ZipFile(zip_csv_file, "w", zipfile.ZIP_DEFLATED) as zf: + csv_content = io.StringIO() + writer = csv.DictWriter(csv_content, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + zf.writestr("users_csv_zip.csv", csv_content.getvalue()) + + # Create zipped CSV fixture with multiple files (should pick matching name) + multi_zip_csv_file = fixtures_path / "users_csv_multi.csv.zip" + with zipfile.ZipFile(multi_zip_csv_file, "w", zipfile.ZIP_DEFLATED) as zf: + # Add a different CSV first + other_content = io.StringIO() + other_writer = csv.DictWriter(other_content, fieldnames=["other"]) + other_writer.writeheader() + other_writer.writerow({"other": "data"}) + zf.writestr("other.csv", other_content.getvalue()) + + # Add the actual fixture + csv_content = io.StringIO() + writer = csv.DictWriter(csv_content, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + zf.writestr("users_csv_multi.csv", csv_content.getvalue()) + + # Create empty CSV zip file (should raise error) + empty_csv_zip = fixtures_path / "empty_csv.csv.zip" + with zipfile.ZipFile(empty_csv_zip, "w", zipfile.ZIP_DEFLATED): + pass + + # Create zip with no CSV files + no_csv_zip = fixtures_path / "no_csv.csv.zip" + with zipfile.ZipFile(no_csv_zip, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("readme.txt", "No CSV files here") + yield fixtures_path @@ -168,7 +233,7 @@ def test_fixture_not_found(self, temp_fixtures_dir: Path) -> None: open_fixture(temp_fixtures_dir, "nonexistent") assert "Could not find the nonexistent fixture" in str(exc_info.value) - assert "(tried .json, .json.gz, .json.zip with case variations)" in str(exc_info.value) + assert "(tried .json, .json.gz, .json.zip, .csv, .csv.gz, .csv.zip with case variations)" in str(exc_info.value) def test_empty_zip_file(self, temp_fixtures_dir: Path) -> None: """Test error handling for empty zip file.""" @@ -218,6 +283,226 @@ def test_invalid_json_content(self, temp_fixtures_dir: Path) -> None: open_fixture(temp_fixtures_dir, "invalid") +class TestOpenFixtureCSV: + """Test cases for CSV fixture loading.""" + + def test_open_plain_csv_fixture(self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]") -> None: + """Test loading plain CSV fixture.""" + result = open_fixture(temp_fixtures_dir, "users_csv") + assert result == sample_csv_data + # Verify it's a list of dicts + assert isinstance(result, list) + assert all(isinstance(row, dict) for row in result) + + def test_open_gzipped_csv_fixture(self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]") -> None: + """Test loading gzipped CSV fixture.""" + result = open_fixture(temp_fixtures_dir, "users_csv_gz") + assert result == sample_csv_data + + def test_open_zipped_csv_fixture(self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]") -> None: + """Test loading zipped CSV fixture.""" + result = open_fixture(temp_fixtures_dir, "users_csv_zip") + assert result == sample_csv_data + + def test_open_zipped_csv_fixture_multiple_files( + self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading zipped CSV fixture with multiple files, should prefer matching name.""" + result = open_fixture(temp_fixtures_dir, "users_csv_multi") + assert result == sample_csv_data + + def test_csv_values_are_strings(self, temp_fixtures_dir: Path) -> None: + """Test that CSV values are strings (important difference from JSON).""" + result = open_fixture(temp_fixtures_dir, "users_csv") + # CSV returns strings, not integers + assert result[0]["id"] == "1" + assert isinstance(result[0]["id"], str) + + def test_json_priority_over_csv( + self, temp_fixtures_dir: Path, sample_data: "list[dict[str, Any]]", sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test that JSON fixtures are loaded before CSV when both exist.""" + # Create both JSON and CSV fixtures with same name + json_file = temp_fixtures_dir / "priority_format.json" + csv_file = temp_fixtures_dir / "priority_format.csv" + + with open(json_file, "w", encoding="utf-8") as f: + json.dump(sample_data, f) + + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + + # Should load JSON first + result = open_fixture(temp_fixtures_dir, "priority_format") + assert result == sample_data # JSON data, not CSV data + + # Remove JSON, should load CSV + json_file.unlink() + result = open_fixture(temp_fixtures_dir, "priority_format") + assert result == sample_csv_data + + def test_empty_csv_zip_file(self, temp_fixtures_dir: Path) -> None: + """Test error handling for empty CSV zip file.""" + with pytest.raises(ValueError) as exc_info: + open_fixture(temp_fixtures_dir, "empty_csv") + assert "No CSV files found in zip archive" in str(exc_info.value) + + def test_csv_zip_with_no_csv_files(self, temp_fixtures_dir: Path) -> None: + """Test error handling for zip file with no CSV files.""" + with pytest.raises(ValueError) as exc_info: + open_fixture(temp_fixtures_dir, "no_csv") + assert "No CSV files found in zip archive" in str(exc_info.value) + + def test_csv_with_special_characters(self, temp_fixtures_dir: Path) -> None: + """Test CSV with special characters, quotes, and commas.""" + special_data = [ + {"name": "O'Brien", "description": "Has apostrophe"}, + {"name": "Smith, Jr.", "description": "Has comma"}, + {"name": 'Quote"Test', "description": "Has quote"}, + ] + + csv_file = temp_fixtures_dir / "special.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=special_data[0].keys()) + writer.writeheader() + writer.writerows(special_data) + + result = open_fixture(temp_fixtures_dir, "special") + assert result == special_data + + def test_csv_with_unicode(self, temp_fixtures_dir: Path) -> None: + """Test CSV with Unicode characters.""" + unicode_data = [ + {"name": "François", "city": "Zürich"}, + {"name": "日本", "city": "東京"}, + {"name": "Москва", "city": "Россия"}, + ] + + csv_file = temp_fixtures_dir / "unicode.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=unicode_data[0].keys()) + writer.writeheader() + writer.writerows(unicode_data) + + result = open_fixture(temp_fixtures_dir, "unicode") + assert result == unicode_data + + def test_csv_with_embedded_newlines(self, temp_fixtures_dir: Path) -> None: + """Test CSV with embedded newlines in quoted fields (RFC 4180 compliance).""" + # This is a critical test - embedded newlines must be preserved + csv_content = """name,description +Alice,"Line 1 +Line 2" +Bob,"Simple description" +Charlie,"Multiple +embedded +newlines" +""" + csv_file = temp_fixtures_dir / "embedded_newlines.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = open_fixture(temp_fixtures_dir, "embedded_newlines") + assert len(result) == 3 + assert result[0]["name"] == "Alice" + assert result[0]["description"] == "Line 1\nLine 2" # Newline must be preserved + assert result[1]["description"] == "Simple description" + assert result[2]["description"] == "Multiple\nembedded\nnewlines" + + def test_csv_with_embedded_newlines_gzip(self, temp_fixtures_dir: Path) -> None: + """Test gzipped CSV with embedded newlines in quoted fields.""" + csv_content = """name,description +Alice,"Line 1 +Line 2" +""" + gz_file = temp_fixtures_dir / "embedded_newlines_gz.csv.gz" + with gzip.open(gz_file, "wt", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = open_fixture(temp_fixtures_dir, "embedded_newlines_gz") + assert len(result) == 1 + assert result[0]["description"] == "Line 1\nLine 2" # Newline must be preserved + + def test_csv_with_embedded_newlines_zip(self, temp_fixtures_dir: Path) -> None: + """Test zipped CSV with embedded newlines in quoted fields.""" + csv_content = """name,description +Alice,"Line 1 +Line 2" +""" + zip_file = temp_fixtures_dir / "embedded_newlines_zip.csv.zip" + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("embedded_newlines_zip.csv", csv_content) + + result = open_fixture(temp_fixtures_dir, "embedded_newlines_zip") + assert len(result) == 1 + assert result[0]["description"] == "Line 1\nLine 2" # Newline must be preserved + + def test_csv_with_utf8_bom(self, temp_fixtures_dir: Path) -> None: + """Test CSV with UTF-8 BOM (common in Excel exports).""" + # UTF-8 BOM is \ufeff at the start of the file + csv_content = "\ufeffname,value\nAlice,1\nBob,2\n" + csv_file = temp_fixtures_dir / "bom.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = open_fixture(temp_fixtures_dir, "bom") + assert len(result) == 2 + # BOM should be stripped, so first key should be "name" not "\ufeffname" + assert "name" in result[0] + assert "\ufeffname" not in result[0] + assert result[0]["name"] == "Alice" + + def test_csv_with_utf8_bom_gzip(self, temp_fixtures_dir: Path) -> None: + """Test gzipped CSV with UTF-8 BOM.""" + csv_content = "\ufeffname,value\nAlice,1\n" + # Write as bytes to preserve BOM + gz_file = temp_fixtures_dir / "bom_gz.csv.gz" + with gzip.open(gz_file, "wb") as f: + f.write(csv_content.encode("utf-8")) + + result = open_fixture(temp_fixtures_dir, "bom_gz") + assert len(result) == 1 + assert "name" in result[0] + assert "\ufeffname" not in result[0] + + def test_csv_with_utf8_bom_zip(self, temp_fixtures_dir: Path) -> None: + """Test zipped CSV with UTF-8 BOM.""" + csv_content = "\ufeffname,value\nAlice,1\n" + zip_file = temp_fixtures_dir / "bom_zip.csv.zip" + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zf: + # Write as bytes to preserve BOM + zf.writestr("bom_zip.csv", csv_content.encode("utf-8")) + + result = open_fixture(temp_fixtures_dir, "bom_zip") + assert len(result) == 1 + assert "name" in result[0] + assert "\ufeffname" not in result[0] + + def test_csv_empty_data_rows(self, temp_fixtures_dir: Path) -> None: + """Test CSV with headers only (no data rows).""" + csv_content = "name,email,age\n" + csv_file = temp_fixtures_dir / "headers_only.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = open_fixture(temp_fixtures_dir, "headers_only") + assert result == [] # Empty list, no rows + + def test_uppercase_plain_csv(self, temp_fixtures_dir: Path) -> None: + """Test loading uppercase plain CSV file.""" + csv_content = "name,value\nUPPER,1\n" + csv_file = temp_fixtures_dir / "UPPERTEST.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + # Should find UPPERTEST.csv when searching for "uppertest" + result = open_fixture(temp_fixtures_dir, "uppertest") + assert len(result) == 1 + assert result[0]["name"] == "UPPER" + + class TestOpenFixtureAsync: """Test cases for asynchronous open_fixture_async function.""" @@ -300,7 +585,7 @@ async def test_fixture_not_found_async(self, temp_fixtures_dir: Path) -> None: await open_fixture_async(temp_fixtures_dir, "nonexistent") assert "Could not find the nonexistent fixture" in str(exc_info.value) - assert "(tried .json, .json.gz, .json.zip with case variations)" in str(exc_info.value) + assert "(tried .json, .json.gz, .json.zip, .csv, .csv.gz, .csv.zip with case variations)" in str(exc_info.value) @pytest.mark.asyncio async def test_empty_zip_file_async(self, temp_fixtures_dir: Path) -> None: @@ -331,6 +616,155 @@ async def test_missing_anyio_dependency(self, temp_fixtures_dir: Path) -> None: # In practice, anyio is a required dependency for this project. pass + # Async CSV Tests + @pytest.mark.asyncio + async def test_open_plain_csv_fixture_async( + self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading plain CSV fixture asynchronously.""" + result = await open_fixture_async(temp_fixtures_dir, "users_csv") + assert result == sample_csv_data + + @pytest.mark.asyncio + async def test_open_gzipped_csv_fixture_async( + self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading gzipped CSV fixture asynchronously.""" + result = await open_fixture_async(temp_fixtures_dir, "users_csv_gz") + assert result == sample_csv_data + + @pytest.mark.asyncio + async def test_open_zipped_csv_fixture_async( + self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading zipped CSV fixture asynchronously.""" + result = await open_fixture_async(temp_fixtures_dir, "users_csv_zip") + assert result == sample_csv_data + + @pytest.mark.asyncio + async def test_open_zipped_csv_fixture_multiple_files_async( + self, temp_fixtures_dir: Path, sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading zipped CSV fixture with multiple files asynchronously.""" + result = await open_fixture_async(temp_fixtures_dir, "users_csv_multi") + assert result == sample_csv_data + + @pytest.mark.asyncio + async def test_json_priority_over_csv_async( + self, temp_fixtures_dir: Path, sample_data: "list[dict[str, Any]]", sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test that JSON fixtures are loaded before CSV when both exist in async version.""" + json_file = temp_fixtures_dir / "priority_async.json" + csv_file = temp_fixtures_dir / "priority_async.csv" + + with open(json_file, "w", encoding="utf-8") as f: + json.dump(sample_data, f) + + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + + result = await open_fixture_async(temp_fixtures_dir, "priority_async") + assert result == sample_data + + @pytest.mark.asyncio + async def test_empty_csv_zip_file_async(self, temp_fixtures_dir: Path) -> None: + """Test error handling for empty CSV zip file in async version.""" + with pytest.raises(ValueError) as exc_info: + await open_fixture_async(temp_fixtures_dir, "empty_csv") + assert "No CSV files found in zip archive" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_csv_with_embedded_newlines_async(self, temp_fixtures_dir: Path) -> None: + """Test async CSV loading with embedded newlines in quoted fields.""" + csv_content = """name,description +Alice,"Line 1 +Line 2" +""" + csv_file = temp_fixtures_dir / "embedded_async.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = await open_fixture_async(temp_fixtures_dir, "embedded_async") + assert len(result) == 1 + assert result[0]["description"] == "Line 1\nLine 2" + + @pytest.mark.asyncio + async def test_csv_with_embedded_newlines_gzip_async(self, temp_fixtures_dir: Path) -> None: + """Test async gzipped CSV loading with embedded newlines.""" + csv_content = """name,description +Alice,"Line 1 +Line 2" +""" + gz_file = temp_fixtures_dir / "embedded_gz_async.csv.gz" + with gzip.open(gz_file, "wt", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = await open_fixture_async(temp_fixtures_dir, "embedded_gz_async") + assert len(result) == 1 + assert result[0]["description"] == "Line 1\nLine 2" + + @pytest.mark.asyncio + async def test_csv_with_embedded_newlines_zip_async(self, temp_fixtures_dir: Path) -> None: + """Test async zipped CSV loading with embedded newlines.""" + csv_content = """name,description +Alice,"Line 1 +Line 2" +""" + zip_file = temp_fixtures_dir / "embedded_zip_async.csv.zip" + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("embedded_zip_async.csv", csv_content) + + result = await open_fixture_async(temp_fixtures_dir, "embedded_zip_async") + assert len(result) == 1 + assert result[0]["description"] == "Line 1\nLine 2" + + @pytest.mark.asyncio + async def test_csv_with_utf8_bom_async(self, temp_fixtures_dir: Path) -> None: + """Test async CSV loading with UTF-8 BOM.""" + csv_content = "\ufeffname,value\nAlice,1\n" + csv_file = temp_fixtures_dir / "bom_async.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = await open_fixture_async(temp_fixtures_dir, "bom_async") + assert len(result) == 1 + assert "name" in result[0] + assert "\ufeffname" not in result[0] + + @pytest.mark.asyncio + async def test_concurrent_csv_reads(self, temp_fixtures_dir: Path) -> None: + """Test concurrent async reads of the same CSV fixture.""" + import asyncio + + csv_content = "name,value\nAlice,1\nBob,2\n" + csv_file = temp_fixtures_dir / "concurrent.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + # Perform 5 concurrent reads + results = await asyncio.gather(*(open_fixture_async(temp_fixtures_dir, "concurrent") for _ in range(5))) + + # All results should be identical + assert len(results) == 5 + for result in results: + assert len(result) == 2 + assert result[0]["name"] == "Alice" + assert result[1]["name"] == "Bob" + + @pytest.mark.asyncio + async def test_uppercase_plain_csv_async(self, temp_fixtures_dir: Path) -> None: + """Test async loading of uppercase plain CSV file.""" + csv_content = "name,value\nUPPER,1\n" + csv_file = temp_fixtures_dir / "UPPERASYNC.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + f.write(csv_content) + + result = await open_fixture_async(temp_fixtures_dir, "upperasync") + assert len(result) == 1 + assert result[0]["name"] == "UPPER" + class TestIntegration: """Integration tests to ensure compatibility with existing usage patterns.""" @@ -394,3 +828,66 @@ def test_compression_efficiency(self, temp_fixtures_dir: Path, sample_data: "lis zip_data = open_fixture(temp_fixtures_dir, "large") assert json_data == gz_data == zip_data == large_data + + def test_csv_fixture_integration_sync(self, temp_fixtures_dir: Path) -> None: + """Test CSV fixture loading in a realistic scenario.""" + # Create a CSV fixture similar to real-world usage + states_data = [ + {"abbreviation": "AL", "name": "Alabama"}, + {"abbreviation": "AK", "name": "Alaska"}, + {"abbreviation": "AZ", "name": "Arizona"}, + ] + + csv_file = temp_fixtures_dir / "us_state_lookup.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["abbreviation", "name"]) + writer.writeheader() + writer.writerows(states_data) + + # Load fixture + fixture_data = open_fixture(temp_fixtures_dir, "us_state_lookup") + assert fixture_data == states_data + assert len(fixture_data) == 3 + assert fixture_data[0]["name"] == "Alabama" + assert fixture_data[0]["abbreviation"] == "AL" + + @pytest.mark.asyncio + async def test_csv_fixture_integration_async(self, temp_fixtures_dir: Path) -> None: + """Test CSV fixture loading in a realistic async scenario.""" + states_data = [ + {"abbreviation": "CA", "name": "California"}, + {"abbreviation": "TX", "name": "Texas"}, + ] + + csv_file = temp_fixtures_dir / "states_async.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["abbreviation", "name"]) + writer.writeheader() + writer.writerows(states_data) + + fixture_data = await open_fixture_async(temp_fixtures_dir, "states_async") + assert fixture_data == states_data + assert len(fixture_data) == 2 + + def test_mixed_format_directory( + self, temp_fixtures_dir: Path, sample_data: "list[dict[str, Any]]", sample_csv_data: "list[dict[str, str]]" + ) -> None: + """Test loading from directory with mixed JSON and CSV fixtures.""" + # Create JSON fixture + json_file = temp_fixtures_dir / "data_json.json" + with open(json_file, "w", encoding="utf-8") as f: + json.dump(sample_data, f) + + # Create CSV fixture + csv_file = temp_fixtures_dir / "data_csv.csv" + with open(csv_file, "w", encoding="utf-8", newline="") as f: + writer = csv.DictWriter(f, fieldnames=sample_csv_data[0].keys()) + writer.writeheader() + writer.writerows(sample_csv_data) + + # Both should load correctly + json_result = open_fixture(temp_fixtures_dir, "data_json") + csv_result = open_fixture(temp_fixtures_dir, "data_csv") + + assert json_result == sample_data + assert csv_result == sample_csv_data diff --git a/uv.lock b/uv.lock index f2d0f1c04..d16db48bc 100644 --- a/uv.lock +++ b/uv.lock @@ -149,16 +149,19 @@ dev = [ { name = "slotscheck" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-click", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-click", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, + { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-paramlinks" }, { name = "sphinx-togglebutton" }, { name = "sphinx-toolbox" }, @@ -194,16 +197,19 @@ doc = [ { name = "shibuya", version = "2026.1.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-click", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-click", version = "6.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, + { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-paramlinks" }, { name = "sphinx-togglebutton" }, { name = "sphinx-toolbox" }, @@ -1155,22 +1161,21 @@ wheels = [ sphinx = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] [[package]] name = "autodocsumm" -version = "0.2.14" +version = "0.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/03/96/92afe8a7912b327c01f0a8b6408c9556ee13b1aba5b98d587ac7327ff32d/autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77", size = 46357, upload-time = "2024-10-23T18:51:47.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/bc/3f66af9beb683728e06ca08797e4e9d3e44f432f339718cae3ba856a9cad/autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0", size = 14640, upload-time = "2024-10-23T18:51:45.115Z" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/41/93/6bf36291b67ac4a51de04330203717945f6ac1b479768f6aff5e4b486ce7/autodocsumm-0.2.6.tar.gz", hash = "sha256:139ca69c5d6b5cf27c41c7ad9c309663f8ead84be2f6a29eefbcfb5166596cf3", size = 43308, upload-time = "2021-06-28T18:49:04.799Z" } [[package]] name = "babel" @@ -2017,11 +2022,30 @@ wheels = [ name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "dogpile-cache" version = "1.4.1" @@ -4373,7 +4397,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -4393,7 +4417,7 @@ resolution-markers = [ "python_full_version == '3.10.*'", ] dependencies = [ - { name = "docutils", marker = "python_full_version == '3.10.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, @@ -4416,12 +4440,13 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mdit-py-plugins", version = "0.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } wheels = [ @@ -6680,18 +6705,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] -[[package]] -name = "roman-numerals-py" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, -] - [[package]] name = "rsa" version = "4.9.1" @@ -7007,7 +7020,8 @@ resolution-markers = [ dependencies = [ { name = "pygments-styles", marker = "python_full_version >= '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/b94cb04adbb984973fe83fd670dd066514610241d829723f678366e691d2/shibuya-2026.1.9.tar.gz", hash = "sha256:b389f10fd9c07b048e940f32d1e1ac096a2d49736389173ac771b37a10b51fdf", size = 86002, upload-time = "2026-01-09T02:19:14.365Z" } wheels = [ @@ -7057,11 +7071,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/f2/21d6ca70c3cf35d01ae9e01be534bf6b6b103c157a728082a5028350c310/soupsieve-2.8.2.tar.gz", hash = "sha256:78a66b0fdee2ab40b7199dc3e747ee6c6e231899feeaae0b9b98a353afd48fd8", size = 118601, upload-time = "2026-01-18T16:21:31.09Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9a/b4450ccce353e2430621b3bb571899ffe1033d5cd72c9e065110f95b1a63/soupsieve-2.8.2-py3-none-any.whl", hash = "sha256:0f4c2f6b5a5fb97a641cf69c0bd163670a0e45e6d6c01a2107f93a6a6f93c51a", size = 37016, upload-time = "2026-01-18T16:21:29.7Z" }, ] [[package]] @@ -7075,7 +7089,7 @@ dependencies = [ { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "babel", marker = "python_full_version < '3.10'" }, { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "imagesize", marker = "python_full_version < '3.10'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2", marker = "python_full_version < '3.10'" }, @@ -7107,7 +7121,7 @@ dependencies = [ { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "babel", marker = "python_full_version == '3.10.*'" }, { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "imagesize", marker = "python_full_version == '3.10.*'" }, { name = "jinja2", marker = "python_full_version == '3.10.*'" }, { name = "packaging", marker = "python_full_version == '3.10.*'" }, @@ -7129,36 +7143,66 @@ wheels = [ [[package]] name = "sphinx" -version = "8.2.3" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, + { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] @@ -7198,7 +7242,8 @@ resolution-markers = [ ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "uvicorn", version = "0.40.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "watchfiles", marker = "python_full_version >= '3.11'" }, @@ -7241,20 +7286,34 @@ wheels = [ [[package]] name = "sphinx-autodoc-typehints" -version = "3.5.2" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/6a/c0360b115c81d449b3b73bf74b64ca773464d5c7b1b77bda87c5e874853b/sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca", size = 20869, upload-time = "2026-01-02T15:23:45.194Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.6.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", ] dependencies = [ - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/4f/4fd5583678bb7dc8afa69e9b309e6a99ee8d79ad3a4728f4e52fd7cb37c7/sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb", size = 37839, upload-time = "2025-10-16T00:50:15.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/51/6603ed3786a2d52366c66f49bc8afb31ae5c0e33d4a156afcb38d2bac62c/sphinx_autodoc_typehints-3.6.2.tar.gz", hash = "sha256:3d37709a21b7b765ad6e20a04ecefcb229b9eb0007cb24f6ebaa8a4576ea7f06", size = 37574, upload-time = "2026-01-02T21:25:28.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c", size = 21184, upload-time = "2025-10-16T00:50:13.973Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6a/877e8a6ea52fc86d88ce110ebcfe4f8474ff590d8a8d322909673af3da7b/sphinx_autodoc_typehints-3.6.2-py3-none-any.whl", hash = "sha256:9e70bee1f487b087c83ba0f4949604a4630bee396e263a324aae1dc4268d2c0f", size = 20853, upload-time = "2026-01-02T21:25:26.853Z" }, ] [[package]] @@ -7266,7 +7325,7 @@ resolution-markers = [ ] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/0a/5b1e8d0579dbb4ca8114e456ca4a68020bfe8e15c7001f3856be4929ab83/sphinx_click-6.0.0.tar.gz", hash = "sha256:f5d664321dc0c6622ff019f1e1c84e58ce0cecfddeb510e004cf60c2a3ab465b", size = 29574, upload-time = "2024-05-15T14:49:17.044Z" } @@ -7287,9 +7346,11 @@ resolution-markers = [ ] dependencies = [ { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "docutils", marker = "python_full_version >= '3.10'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/ed/a9767cd1b8b7fbdf260a89d5c8c86e20e3536b9878579e5ab7965a291e55/sphinx_click-6.2.0.tar.gz", hash = "sha256:fc78b4154a4e5159462e36de55b8643747da6cda86b3b52a8bb62289e603776c", size = 27035, upload-time = "2025-12-04T19:33:05.437Z" } wheels = [ @@ -7303,7 +7364,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } wheels = [ @@ -7314,16 +7376,38 @@ wheels = [ name = "sphinx-design" version = "0.6.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] dependencies = [ { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, ] +[[package]] +name = "sphinx-design" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz", hash = "sha256:d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a", size = 2203582, upload-time = "2026-01-19T13:12:53.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, +] + [[package]] name = "sphinx-jinja2-compat" version = "0.4.1" @@ -7343,10 +7427,12 @@ name = "sphinx-paramlinks" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363, upload-time = "2023-08-11T16:09:28.604Z" } @@ -7358,7 +7444,7 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "docutils", marker = "python_full_version < '3.10'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pygments", marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, ] @@ -7376,7 +7462,7 @@ resolution-markers = [ ] dependencies = [ { name = "certifi", marker = "python_full_version == '3.10.*'" }, - { name = "docutils", marker = "python_full_version == '3.10.*'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "idna", marker = "python_full_version == '3.10.*'" }, { name = "pygments", marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, @@ -7399,12 +7485,13 @@ resolution-markers = [ ] dependencies = [ { name = "certifi", marker = "python_full_version >= '3.11'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "idna", marker = "python_full_version >= '3.11'" }, { name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/a3/91293c0e0f0b76d0697ba7a41541929ca3f5457671d008bd84a9bde17e21/sphinx_prompt-1.10.2.tar.gz", hash = "sha256:47b592ba75caebd044b0eddf7a5a1b6e0aef6df587b034377cd101a999b686ba", size = 5566, upload-time = "2025-11-28T09:23:18.057Z" } @@ -7417,11 +7504,13 @@ name = "sphinx-tabs" version = "3.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pygments" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/32/ab475e252dc2b704e82a91141fa404cdd8901a5cf34958fd22afacebfccd/sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531", size = 16070, upload-time = "2024-01-21T12:13:39.392Z" } wheels = [ @@ -7433,11 +7522,13 @@ name = "sphinx-togglebutton" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "setuptools" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "wheel" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/6b/19def5241b45a7ae90fd91052bb91fa7b8fbcc0606a0cf65ac4ea70fb93b/sphinx_togglebutton-0.4.4.tar.gz", hash = "sha256:04c332692fd5f5363ad02a001e693369767d6c1f0e58279770a2aeb571b472a1", size = 17883, upload-time = "2026-01-14T14:33:11.599Z" } @@ -7456,7 +7547,8 @@ dependencies = [ { name = "cachecontrol", version = "0.14.3", source = { registry = "https://pypi.org/simple" }, extra = ["filecache"], marker = "python_full_version < '3.10'" }, { name = "cachecontrol", version = "0.14.4", source = { registry = "https://pypi.org/simple" }, extra = ["filecache"], marker = "python_full_version >= '3.10'" }, { name = "dict2css" }, - { name = "docutils" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "domdf-python-tools" }, { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -7466,10 +7558,12 @@ dependencies = [ { name = "ruamel-yaml" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autodoc-typehints", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx-autodoc-typehints", version = "3.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx-autodoc-typehints", version = "3.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-jinja2-compat" }, { name = "sphinx-prompt", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx-prompt", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, @@ -7550,7 +7644,8 @@ dependencies = [ { name = "jinja2", marker = "python_full_version >= '3.10'" }, { name = "pyyaml", marker = "python_full_version >= '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/a5/65a5c439cc14ba80483b9891e9350f11efb80cd3bdccb222f0c738068c78/sphinxcontrib_mermaid-2.0.0.tar.gz", hash = "sha256:cf4f7d453d001132eaba5d1fdf53d42049f02e913213cf8337427483bfca26f4", size = 18194, upload-time = "2026-01-13T17:13:42.563Z" } wheels = [