diff --git a/docs/participant-uploads.md b/docs/participant-uploads.md
new file mode 100644
index 00000000..1236b606
--- /dev/null
+++ b/docs/participant-uploads.md
@@ -0,0 +1,28 @@
+# Participant uploads and account timestamps
+
+Profile image writes accept only base64 data URLs containing actual PNG, JPEG
+or WebP images, at most 1 MiB decoded and 16 million pixels. URLs, SVG, animated
+images, type mismatches and malformed images are rejected. Pillow decodes and
+re-encodes only pixels so metadata and trailing payloads are not stored.
+
+CV writes accept PDF only, at most 1 MiB and 20 pages. The parsed object graph
+is inspected for JavaScript, active actions, embedded files and rich media;
+encrypted and malformed PDFs are rejected. The stored PDF is rebuilt from
+pages without annotations, document metadata or attachments. This is file
+validation and sanitisation, not an antivirus or a guarantee against every
+possible vulnerability in a PDF viewer. Keep the parsers updated.
+
+These rules apply at request-schema validation for account creation, profile
+updates, and event registration/update. Read schemas do not reinterpret legacy
+values. Existing stored files are not retroactively sanitised. Deploy the
+backend together with the frontend: browser restrictions alone can be bypassed.
+
+The request body limit is 3 MiB so a profile request containing a 1 MiB image
+and 1 MiB CV, with base64 overhead, can pass transport validation; each file
+still has its own 1 MiB limit. Review any proxy or deployment override of
+RATE_LIMIT__MAX_BODY_BYTES if large, valid uploads are rejected before the API.
+
+New users receive a fresh UTC creation timestamp on each INSERT, rather than
+the date captured when the module was imported. Profile responses include the
+time and UTC offset. This fixes new records; dates already stored incorrectly
+cannot be inferred or repaired without a trustworthy audit trail.
diff --git a/docs/request-protection.md b/docs/request-protection.md
index bb233d53..2d5621c4 100644
--- a/docs/request-protection.md
+++ b/docs/request-protection.md
@@ -15,7 +15,7 @@ Redis instance, prefix and signing secret. No in-memory fallback exists.
| Password recovery, verification resend, contact, per IP | 20/hour combined | `MAIL_PER_HOUR` |
| Signup and public mail routes, per normalized email | 3/15 minutes combined | `MAIL_PER_RECIPIENT` |
| Signup and public mail routes, across all clients | 300/hour combined | `MAIL_TOTAL_PER_HOUR` |
-| Request body | 1 MiB, including streamed requests | `MAX_BODY_BYTES` |
+| Request body | 3 MiB, including streamed requests (two 1 MiB files encoded as base64 plus JSON) | `MAX_BODY_BYTES` |
| Receiving the complete request body | 10 seconds | `BODY_TIMEOUT_SECONDS` |
Budgets count attempts, including invalid credentials/payloads; not just successful
diff --git a/pyproject.toml b/pyproject.toml
index b8618852..3a7e25f0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,6 +24,8 @@ dependencies = [
"uvicorn>=0.30.0",
"redis>=5,<7",
"segno>=1.6",
+ "pillow>=12.0",
+ "pypdf>=6.0",
]
[tool.hatch.build.targets.wheel]
diff --git a/src/configuration/Settings.py b/src/configuration/Settings.py
index 532e6835..4b01944d 100644
--- a/src/configuration/Settings.py
+++ b/src/configuration/Settings.py
@@ -103,7 +103,7 @@ class RateLimitSettings(BaseSettings):
mail_per_hour: int = Field(default=20, ge=1)
mail_per_recipient: int = Field(default=3, ge=1)
mail_total_per_hour: int = Field(default=300, ge=1)
- max_body_bytes: int = Field(default=1048576, ge=1024)
+ max_body_bytes: int = Field(default=3145728, ge=1024)
body_timeout_seconds: float = Field(default=10, gt=0)
diff --git a/src/impl/Event/schema.py b/src/impl/Event/schema.py
index eed6403d..d29bf24c 100644
--- a/src/impl/Event/schema.py
+++ b/src/impl/Event/schema.py
@@ -1,6 +1,7 @@
# from __future__ import annotations
from datetime import datetime
from typing import Optional
+from src.utils.uploads import Curriculum
from pydantic import Field, field_validator
@@ -108,7 +109,7 @@ class EventUpdate(BaseSchema):
class HackerEventRegistration(BaseSchema):
shirt_size: str
food_restrictions: str
- cv: Optional[str] = None
+ cv: Optional[Curriculum] = None
description: Optional[str] = None
github: Optional[str] = None
linkedin: Optional[str] = None
@@ -130,7 +131,7 @@ def shirt_size_validation(cls, v):
class HackerEventRegistrationUpdate(BaseSchema):
shirt_size: Optional[str] = None
food_restrictions: Optional[str] = None
- cv: Optional[str] = None
+ cv: Optional[Curriculum] = None
description: Optional[str] = None
github: Optional[str] = None
linkedin: Optional[str] = None
diff --git a/src/impl/Hacker/schema.py b/src/impl/Hacker/schema.py
index a39aaf27..044ace6c 100644
--- a/src/impl/Hacker/schema.py
+++ b/src/impl/Hacker/schema.py
@@ -1,4 +1,5 @@
from typing import Optional
+from src.utils.uploads import Curriculum
from src.impl.User.schema import UserCreate, UserGet, UserGetAll, UserUpdate
@@ -9,7 +10,7 @@ class HackerCreate(UserCreate):
study_center: Optional[str] = None
location: Optional[str] = None
how_did_you_meet_us: Optional[str] = None
- cv: Optional[str] = None
+ cv: Optional[Curriculum] = None
class HackerGet(UserGet):
@@ -35,4 +36,4 @@ class HackerUpdate(UserUpdate):
study_center: Optional[str] = None
location: Optional[str] = None
how_did_you_meet_us: Optional[str] = None
- cv: Optional[str] = None
+ cv: Optional[Curriculum] = None
diff --git a/src/impl/User/model.py b/src/impl/User/model.py
index a78dd568..f6dccd21 100644
--- a/src/impl/User/model.py
+++ b/src/impl/User/model.py
@@ -1,4 +1,4 @@
-from datetime import date
+from datetime import date, datetime, timezone
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
@@ -23,8 +23,12 @@ class User(BaseModel):
address: Mapped[Optional[str]] = mapped_column(String)
shirt_size: Mapped[Optional[str]] = mapped_column(String)
type: Mapped[Optional[str]] = mapped_column(String)
- created_at: Mapped[Optional[date]] = mapped_column(DateTime, default=date.today())
- updated_at: Mapped[Optional[date]] = mapped_column(DateTime, default=date.today())
+ created_at: Mapped[Optional[datetime]] = mapped_column(
+ DateTime, default=lambda: datetime.now(timezone.utc).replace(tzinfo=None)
+ )
+ updated_at: Mapped[Optional[datetime]] = mapped_column(
+ DateTime, default=lambda: datetime.now(timezone.utc).replace(tzinfo=None)
+ )
image: Mapped[str] = mapped_column(String, default="")
# is_image_url: bool = mapped_column(Boolean, default=False)
code: Mapped[str] = mapped_column(String, default="", unique=True, index=True)
diff --git a/src/impl/User/schema.py b/src/impl/User/schema.py
index a371bfa2..a139d1f4 100644
--- a/src/impl/User/schema.py
+++ b/src/impl/User/schema.py
@@ -1,8 +1,9 @@
import re
-from datetime import date
+from datetime import date, datetime, timezone
from typing import Optional
-from pydantic import field_validator
+from pydantic import field_validator, field_serializer
+from src.utils.uploads import ProfileImage
from src.impl.UserConfig.schema import UserConfigCreate, UserConfigGetAll
from src.utils.Base.BaseSchema import BaseSchema
@@ -19,7 +20,7 @@ class UserCreate(BaseSchema):
telephone: str
address: Optional[str] = None
shirt_size: Optional[str] = None
- image: Optional[str] = None
+ image: Optional[ProfileImage] = None
config: UserConfigCreate
# is_image_url: Optional[bool] = None = None
# recive_mails: Optional[bool] = None = None
@@ -63,10 +64,14 @@ def shirt_size_validation(cls, v):
class UserGet(BaseSchema):
name: str
nickname: str
- created_at: date
+ created_at: datetime
type: str
image: Optional[str] = None
+ @field_serializer("created_at")
+ def serialize_created_at(self, value):
+ return value.replace(tzinfo=timezone.utc) if value.tzinfo is None else value
+
class UserGetAll(UserGet):
id: int
@@ -92,7 +97,7 @@ class UserUpdate(BaseSchema):
telephone: Optional[str] = None
address: Optional[str] = None
shirt_size: Optional[str] = None
- image: Optional[str] = None
+ image: Optional[ProfileImage] = None
# is_image_url: Optional[bool] = None
# recive_mails: Optional[bool] = None
diff --git a/src/utils/uploads.py b/src/utils/uploads.py
new file mode 100644
index 00000000..bc6f7994
--- /dev/null
+++ b/src/utils/uploads.py
@@ -0,0 +1,126 @@
+"""Validate untrusted participant uploads and store a canonical, inert format."""
+import base64
+import binascii
+import io
+import warnings
+from typing import Annotated
+
+from PIL import Image, UnidentifiedImageError
+from pydantic import AfterValidator
+from pypdf import PdfReader, PdfWriter
+from pypdf.generic import ArrayObject, DictionaryObject, IndirectObject
+
+MAX_FILE_BYTES = 1024 * 1024
+MAX_IMAGE_PIXELS = 16_000_000
+MIME_FORMATS = {"image/png": "PNG", "image/jpeg": "JPEG", "image/webp": "WEBP"}
+
+
+def decode_upload(value, allowed):
+ if len(value) > 4 * ((MAX_FILE_BYTES + 2) // 3) + 64:
+ raise ValueError("File exceeds 1 MiB")
+ header, separator, encoded = value.partition(",")
+ if not separator or not header.startswith("data:") or not header.endswith(";base64"):
+ raise ValueError("Upload a file, not a URL")
+ mime = header[5:-7]
+ if mime not in allowed:
+ raise ValueError("Unsupported file type")
+ try:
+ raw = base64.b64decode(encoded, validate=True)
+ except (ValueError, binascii.Error) as exc:
+ raise ValueError("Invalid file encoding") from exc
+ if not raw or len(raw) > MAX_FILE_BYTES:
+ raise ValueError("File must be non-empty and at most 1 MiB")
+ return mime, raw
+
+
+def encode_upload(mime, raw):
+ if len(raw) > MAX_FILE_BYTES:
+ raise ValueError("Processed file exceeds 1 MiB")
+ return f"data:{mime};base64," + base64.b64encode(raw).decode("ascii")
+
+
+def validate_image(value):
+ if value in (None, ""):
+ return value
+ mime, raw = decode_upload(value, MIME_FORMATS)
+ try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", Image.DecompressionBombWarning)
+ with Image.open(io.BytesIO(raw)) as image:
+ if image.format != MIME_FORMATS[mime]:
+ raise ValueError("Image content does not match its type")
+ if image.width * image.height > MAX_IMAGE_PIXELS or getattr(image, "n_frames", 1) != 1:
+ raise ValueError("Image dimensions or animation are not supported")
+ image.verify()
+ with Image.open(io.BytesIO(raw)) as image:
+ # Copy only decoded pixels: discard EXIF, embedded text, trailing
+ # polyglot payloads and all other user-provided metadata.
+ mode = "RGB" if mime == "image/jpeg" else "RGBA"
+ clean = Image.new(mode, image.size)
+ clean.paste(image.convert(mode))
+ output = io.BytesIO()
+ clean.save(output, format=MIME_FORMATS[mime])
+ return encode_upload(mime, output.getvalue())
+ except (UnidentifiedImageError, OSError, Image.DecompressionBombError,
+ Image.DecompressionBombWarning) as exc:
+ raise ValueError("Invalid or unsafe image") from exc
+
+
+# Reject active content anywhere in the reachable object graph, including
+# encoded PDF names and indirect references (a byte-string search is not enough).
+FORBIDDEN_PDF_KEYS = {"/AA", "/OpenAction", "/JS", "/JavaScript", "/EmbeddedFiles",
+ "/EF", "/XFA", "/RichMediaContent", "/RichMediaSettings"}
+FORBIDDEN_PDF_ACTIONS = {"/JavaScript", "/Launch", "/SubmitForm", "/ImportData",
+ "/GoToR", "/GoToE", "/Rendition", "/RichMediaExecute"}
+
+
+def validate_cv(value):
+ if value in (None, ""):
+ return value
+ mime, raw = decode_upload(value, {"application/pdf"})
+ if not raw.startswith(b"%PDF-"):
+ raise ValueError("CV must contain a PDF document")
+ try:
+ reader = PdfReader(io.BytesIO(raw), strict=True)
+ if reader.is_encrypted:
+ raise ValueError("Encrypted PDFs are not supported")
+ visited = set()
+ pending = [reader.trailer]
+ count = 0
+ while pending:
+ obj = pending.pop()
+ count += 1
+ if count > 50_000:
+ raise ValueError("PDF is too complex")
+ if isinstance(obj, IndirectObject):
+ key = (obj.idnum, obj.generation)
+ if key in visited:
+ continue
+ visited.add(key)
+ obj = obj.get_object()
+ if isinstance(obj, DictionaryObject):
+ if FORBIDDEN_PDF_KEYS.intersection(obj.keys()) or obj.get("/S") in FORBIDDEN_PDF_ACTIONS:
+ raise ValueError("PDF contains active content or attachments")
+ if obj.get("/Type") == "/EmbeddedFile" or obj.get("/Subtype") in {"/RichMedia", "/FileAttachment"}:
+ raise ValueError("PDF contains embedded content")
+ pending.extend(obj.values())
+ elif isinstance(obj, ArrayObject):
+ pending.extend(obj)
+ if not 1 <= len(reader.pages) <= 20:
+ raise ValueError("CV must contain between 1 and 20 pages")
+ writer = PdfWriter()
+ for page in reader.pages:
+ # Retain page content, but not actions, interactive widgets, links,
+ # document metadata, forms or trailing non-PDF payloads.
+ writer.add_page(page, excluded_keys=["/Annots", "/AA"])
+ output = io.BytesIO()
+ writer.write(output)
+ return encode_upload(mime, output.getvalue())
+ except ValueError:
+ raise
+ except Exception as exc:
+ raise ValueError("Invalid or unsafe PDF") from exc
+
+
+ProfileImage = Annotated[str, AfterValidator(validate_image)]
+Curriculum = Annotated[str, AfterValidator(validate_cv)]
diff --git a/tests/test_privacy.py b/tests/test_privacy.py
index 705c75e5..85699d8b 100644
--- a/tests/test_privacy.py
+++ b/tests/test_privacy.py
@@ -25,7 +25,7 @@ def test_pending_profile_does_not_expose_verification_token(client, signup_paylo
assert_no_credentials(response.json())
-def test_verification_response_has_no_credentials(client, signup_payload, engine):
+def test_verification_returns_only_the_new_verified_session(client, signup_payload, engine):
from src.impl.User.model import User
signup = client.post("/v1/hacker/signup", json=signup_payload).json()
@@ -33,8 +33,18 @@ def test_verification_response_has_no_credentials(client, signup_payload, engine
token = session.get(User, signup["user_id"]).verification_token
response = client.post("/v1/auth/verify", params={"token": token})
assert response.status_code == 200, response.text
- assert response.json() == {"success": True}
- assert_no_credentials(response.json())
+ # Verification now intentionally starts a session (existing API contract).
+ # Check the narrow response and ownership rather than treating the newly
+ # issued session tokens as leaked database credentials.
+ result = response.json()
+ assert set(result) == {"success", "user_id", "access_token", "refresh_token", "token_type"}
+ assert result["success"] is True
+ assert result["user_id"] == signup["user_id"]
+ profile = client.get("/v1/auth/me", headers={"Authorization": f"Bearer {result['access_token']}"})
+ assert profile.status_code == 200
+ assert profile.json()["id"] == signup["user_id"]
+ assert profile.json()["is_verified"] is True
+ assert_no_credentials(profile.json())
def test_hacker_cannot_read_organizer_nif(client, signup_payload, engine):
diff --git a/tests/test_uploads.py b/tests/test_uploads.py
new file mode 100644
index 00000000..de4ffa19
--- /dev/null
+++ b/tests/test_uploads.py
@@ -0,0 +1,134 @@
+import base64
+import io
+from datetime import datetime, timedelta, timezone
+
+import pytest
+from PIL import Image
+from pypdf import PdfReader, PdfWriter
+
+from src.utils.uploads import validate_image, validate_cv, MAX_FILE_BYTES
+
+
+def data_uri(mime, raw):
+ return f"data:{mime};base64," + base64.b64encode(raw).decode()
+
+
+def png():
+ output = io.BytesIO()
+ Image.new("RGB", (8, 8), "orange").save(output, format="PNG")
+ return output.getvalue()
+
+
+def pdf(writer=None):
+ writer = writer or PdfWriter()
+ writer.add_blank_page(width=200, height=200)
+ output = io.BytesIO()
+ writer.write(output)
+ return output.getvalue()
+
+
+@pytest.mark.parametrize("value", ["https://example.test/photo.png", data_uri("image/svg+xml", b''), data_uri("image/png", b""), "data:image/png;base64,@@@"])
+def test_rejects_urls_scripts_and_fake_images(value):
+ with pytest.raises(ValueError):
+ validate_image(value)
+
+
+@pytest.mark.parametrize("format,mime", [("PNG", "image/png"), ("JPEG", "image/jpeg"), ("WEBP", "image/webp")])
+def test_images_are_decoded_and_reencoded_without_trailing_payload(format, mime):
+ output = io.BytesIO()
+ Image.new("RGB", (8, 8), "orange").save(output, format=format)
+ clean = validate_image(data_uri(mime, output.getvalue() + b""))
+ raw = base64.b64decode(clean.split(",")[1])
+ assert b""))
+ raw = base64.b64decode(cleaned.split(",")[1])
+ assert b"untrusted-metadata" not in raw
+ assert b"")
+ assert client.put(f"/v1/event/{event}/update-register/{user.id}", headers=user.headers,
+ json={"cv": bad_cv}).status_code == 422
+ response = client.put(f"/v1/hacker/{user.id}", headers=user.headers,
+ json={"cv": data_uri("application/pdf", pdf())})
+ assert response.status_code == 200, response.text
+
+
+def test_creation_timestamp_is_computed_for_each_new_user(monkeypatch):
+ from src.impl.User import model
+ column = model.User.__table__.c.created_at
+ assert column.default.is_callable
+ first = column.default.arg(None)
+ future = datetime.now(timezone.utc) + timedelta(days=2)
+ class Clock:
+ @staticmethod
+ def now(tz):
+ return future
+ monkeypatch.setattr(model, "datetime", Clock)
+ assert column.default.arg(None) - first > timedelta(days=1)
+
+
+def test_profile_creation_timestamp_contains_time_and_timezone(client, create_user):
+ user = create_user()
+ response = client.get(f"/v1/hacker/{user.id}", headers=user.headers)
+ assert response.status_code == 200
+ created = datetime.fromisoformat(response.json()["created_at"].replace("Z", "+00:00"))
+ assert created.tzinfo is not None
+ assert abs((datetime.now(timezone.utc) - created).total_seconds()) < 10
+
+
+def test_valid_file_under_one_mib_survives_base64_request_overhead(client, create_user):
+ import os
+ user = create_user()
+ output = io.BytesIO()
+ Image.frombytes("RGB", (576, 480), os.urandom(576 * 480 * 3)).save(output, format="PNG")
+ raw = output.getvalue()
+ image = data_uri("image/png", raw)
+ assert len(raw) < MAX_FILE_BYTES < len(image)
+ response = client.put(f"/v1/hacker/{user.id}", headers=user.headers, json={"image": image})
+ assert response.status_code == 200, response.text
diff --git a/uv.lock b/uv.lock
index c016f156..9c0d0be7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -399,10 +399,12 @@ dependencies = [
{ name = "gunicorn" },
{ name = "openapi-python-client" },
{ name = "passlib" },
+ { name = "pillow" },
{ name = "psycopg2-binary" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "pyjwt", extra = ["crypto"] },
+ { name = "pypdf" },
{ name = "python-dateutil" },
{ name = "pyyaml" },
{ name = "redis" },
@@ -426,10 +428,12 @@ requires-dist = [
{ name = "gunicorn", specifier = ">=22.0.0" },
{ name = "openapi-python-client", specifier = ">=0.20.0" },
{ name = "passlib", specifier = ">=1.7.4" },
+ { name = "pillow", specifier = ">=12.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.9" },
{ name = "pydantic", specifier = ">=2.7.1" },
{ name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.13.0" },
+ { name = "pypdf", specifier = ">=6.0" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
{ name = "pyyaml", specifier = ">=6.0.1" },
{ name = "redis", specifier = ">=5,<7" },
@@ -578,6 +582,77 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
+[[package]]
+name = "pillow"
+version = "12.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
+ { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
+ { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
+ { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
+ { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
+ { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
+ { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
+ { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
+ { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
+ { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
+ { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
+ { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
+ { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
+ { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
+ { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
+ { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
+ { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
+ { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
+ { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
+ { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
+ { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
+ { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
+ { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
+ { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
+ { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
+]
+
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -775,6 +850,15 @@ crypto = [
{ name = "cryptography" },
]
+[[package]]
+name = "pypdf"
+version = "6.19.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" },
+]
+
[[package]]
name = "pytest"
version = "9.1.1"