renderers 0.1.9.dev1__tar.gz → 0.1.9.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/PKG-INFO +1 -1
  2. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/_version.py +2 -2
  3. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/configs.py +15 -2
  4. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/parsing.py +19 -6
  5. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/qwen3.py +20 -1
  6. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/qwen35.py +20 -3
  7. renderers-0.1.9.dev3/tests/test_disabled_thinking_stability.py +226 -0
  8. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_multimodal.py +20 -0
  9. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_prime_qwen3_parity.py +58 -0
  10. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_renderer_config_parity.py +25 -0
  11. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/uv.lock +49 -70
  12. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.github/workflows/publish-dev.yml +0 -0
  13. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.github/workflows/publish.yml +0 -0
  14. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.github/workflows/style.yml +0 -0
  15. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.github/workflows/test.yml +0 -0
  16. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.gitignore +0 -0
  17. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/.pre-commit-config.yaml +0 -0
  18. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/LICENSE +0 -0
  19. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/README.md +0 -0
  20. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/docs/renderer-config.md +0 -0
  21. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/README.md +0 -0
  22. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/sglang/multiturn_generate_sglang.py +0 -0
  23. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/sglang/online_multiturn_sglang.py +0 -0
  24. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/tinker/multiturn_generate_tinker.py +0 -0
  25. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/transformers/multiturn_generate_transformers.py +0 -0
  26. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/examples/vllm/multiturn_generate_vllm.py +0 -0
  27. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/pyproject.toml +0 -0
  28. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/__init__.py +0 -0
  29. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/base.py +0 -0
  30. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/client.py +0 -0
  31. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/deepseek_r1.py +0 -0
  32. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/deepseek_v3.py +0 -0
  33. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/default.py +0 -0
  34. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/glm45.py +0 -0
  35. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/glm5.py +0 -0
  36. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/gpt_oss.py +0 -0
  37. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/hy3.py +0 -0
  38. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/kimi_k2.py +0 -0
  39. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/kimi_k25.py +0 -0
  40. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/laguna_xs2.py +0 -0
  41. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/llama_3.py +0 -0
  42. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/minimax_m2.py +0 -0
  43. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/nemotron3.py +0 -0
  44. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/parsers.py +0 -0
  45. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/prime_qwen3.py +0 -0
  46. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/qwen36.py +0 -0
  47. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/renderers/qwen3_vl.py +0 -0
  48. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/conftest.py +0 -0
  49. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_bridge.py +0 -0
  50. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_build_helpers.py +0 -0
  51. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_client.py +0 -0
  52. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_deepseek_r1.py +0 -0
  53. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_gpt_oss_harmony_parity.py +0 -0
  54. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_hy3.py +0 -0
  55. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_incremental.py +0 -0
  56. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_is_content.py +0 -0
  57. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_kimi_k25_tool_schema.py +0 -0
  58. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_laguna_xs21.py +0 -0
  59. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_llama_3.py +0 -0
  60. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_load_tokenizer.py +0 -0
  61. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_message_indices.py +0 -0
  62. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_message_tool_names.py +0 -0
  63. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_nemotron3_parity.py +0 -0
  64. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_nemotron3_ultra.py +0 -0
  65. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_parse_response.py +0 -0
  66. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_parse_response_robustness.py +0 -0
  67. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_parsers.py +0 -0
  68. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_preserve_thinking.py +0 -0
  69. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_qwen35_size_coverage.py +0 -0
  70. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_render_ids.py +0 -0
  71. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_renderer_config.py +0 -0
  72. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_roundtrip.py +0 -0
  73. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_sampled_mask.py +0 -0
  74. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_tokens_per_message.py +0 -0
  75. {renderers-0.1.9.dev1 → renderers-0.1.9.dev3}/tests/test_tool_arg_type_preservation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: renderers
3
- Version: 0.1.9.dev1
3
+ Version: 0.1.9.dev3
4
4
  Summary: Chat template renderers — deterministic message-to-token conversion for LLM training
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.9.dev1'
22
- __version_tuple__ = version_tuple = (0, 1, 9, 'dev1')
21
+ __version__ = version = '0.1.9.dev3'
22
+ __version_tuple__ = version_tuple = (0, 1, 9, 'dev3')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -178,7 +178,13 @@ class Qwen3RendererConfig(BaseRendererConfig):
178
178
  enable_thinking: bool = True
179
179
  """When ``True``, the generation prompt includes ``<think>`` so the
180
180
  model continues into a thinking block. Mirrors the chat template's
181
- ``enable_thinking`` kwarg."""
181
+ ``enable_thinking`` kwarg.
182
+
183
+ When ``False``, the renderer deliberately deviates from the template
184
+ on historical assistant turns without ``reasoning_content``: the empty
185
+ ``<think>\\n\\n</think>\\n\\n`` wrapper the generation prompt prefilled
186
+ is re-emitted instead of stripped, keeping re-renders token-stable with
187
+ the sampled stream (see ``renderers/qwen3.py`` module docstring)."""
182
188
 
183
189
 
184
190
  class PrimeQwen3RendererConfig(BaseRendererConfig):
@@ -196,7 +202,14 @@ class Qwen35RendererConfig(BaseRendererConfig):
196
202
  """When ``True``, the generation prompt includes ``<think>``. ``None``
197
203
  auto-detects from the tokenizer's chat-template default — Instruct
198
204
  checkpoints default off, Thinking checkpoints default on. Mirrors
199
- the chat template's ``enable_thinking`` kwarg."""
205
+ the chat template's ``enable_thinking`` kwarg.
206
+
207
+ When resolved ``False``, the renderer deliberately deviates from the
208
+ template on historical assistant turns without ``reasoning_content``:
209
+ the empty ``<think>\\n\\n</think>\\n\\n`` wrapper the generation prompt
210
+ prefilled is re-emitted instead of stripped, keeping re-renders
211
+ token-stable with the sampled stream (see ``renderers/qwen35.py``
212
+ module docstring)."""
200
213
 
201
214
  add_vision_id: bool = False
202
215
  """When ``True``, prefix each ``<|vision_start|>`` placeholder with
@@ -286,7 +286,15 @@ def parse_qwen35(
286
286
  tool_call_end_id: int,
287
287
  tools: list[ToolSpec] | None = None,
288
288
  ) -> ParsedResponse:
289
- """Parse Qwen3.5 completion tokens. XML-style tool calls, token-level thinking."""
289
+ """Parse Qwen3.5 completion tokens. XML-style tool calls, token-level thinking.
290
+
291
+ ``reasoning_content`` contract: ``None`` means the completion carried no
292
+ think block at all; a string (possibly empty) means a think block was
293
+ present. The distinction matters for renderers with key-presence
294
+ semantics (PrimeQwen3Renderer): a sampled ``<think></think>`` must
295
+ round-trip parse → message → render back to the same tokens, so an
296
+ empty-but-present block is ``""``, never ``None``.
297
+ """
290
298
  ids = _strip_stop_tokens(token_ids, stop_ids)
291
299
 
292
300
  # Thinking: find </think> by token ID
@@ -300,12 +308,12 @@ def parse_qwen35(
300
308
  ids = ids[think_end + 1 :]
301
309
  parse_offset = think_end + 1
302
310
  elif think_id in set(ids):
303
- # <think> present but no </think> — truncated reasoning
311
+ # <think> present but no </think> — truncated reasoning. Block
312
+ # present ⇒ string (see docstring), even when nothing follows the
313
+ # opening tag.
304
314
  think_start = _find(ids, think_id)
305
315
  reasoning = _decode(tokenizer, ids[think_start + 1 :]).strip()
306
- return ParsedResponse(
307
- content="", reasoning_content=reasoning or None, tool_calls=[]
308
- )
316
+ return ParsedResponse(content="", reasoning_content=reasoning, tool_calls=[])
309
317
 
310
318
  tc_start = _find(ids, tool_call_id)
311
319
  tool_calls: list[ParsedToolCall] = []
@@ -322,9 +330,14 @@ def parse_qwen35(
322
330
  else:
323
331
  content_text = _decode(tokenizer, ids).strip()
324
332
 
333
+ # ``reasoning`` is None only when no think block was found; keep ""
334
+ # (empty-but-present block) intact so ``<think></think>`` samples
335
+ # round-trip — collapsing it to None made PrimeQwen3Renderer drop the
336
+ # block on re-render, forking the token stream for the rest of the
337
+ # rollout.
325
338
  return ParsedResponse(
326
339
  content=content_text,
327
- reasoning_content=reasoning or None,
340
+ reasoning_content=reasoning,
328
341
  tool_calls=tool_calls,
329
342
  )
330
343
 
@@ -5,6 +5,15 @@ Key differences from Qwen3.5:
5
5
  - Tool calls use JSON format: {"name": "...", "arguments": ...}
6
6
  - Thinking blocks only inserted when loop.last OR reasoning_content present
7
7
  - Generation prompt does NOT add <think> by default
8
+
9
+ One documented deviation from the Jinja template: with
10
+ ``enable_thinking=False`` the empty ``<think>\n\n</think>\n\n`` wrapper is
11
+ re-emitted on historical assistant turns without ``reasoning_content`` (the
12
+ template strips it from turns at or before the last user query). The
13
+ generation prompt prefills that wrapper, so every turn sampled with thinking
14
+ disabled contains it — stripping it on re-render would make the same
15
+ conversation produce different tokens depending on when it is rendered.
16
+ See ``tests/test_disabled_thinking_stability.py``.
8
17
  """
9
18
 
10
19
  from __future__ import annotations
@@ -477,7 +486,17 @@ class Qwen3Renderer:
477
486
  emit_in_template_window = msg_idx > last_query_index and (
478
487
  is_last or reasoning_content
479
488
  )
480
- if emit_in_template_window:
489
+ # With thinking disabled, the generation prompt prefilled the empty
490
+ # ``<think>\n\n</think>\n\n`` wrapper, so it is part of every sampled
491
+ # turn's token stream. Re-emit it on historical turns — deviating
492
+ # from the Jinja template, which strips it from turns at or before
493
+ # the last user query — so re-renders stay token-stable with what
494
+ # the model actually sampled. Turns that carry reasoning_content
495
+ # were not sampled under this config; those stay template-faithful.
496
+ emit_thinking = emit_in_template_window or (
497
+ not self.config.enable_thinking and not reasoning_content
498
+ )
499
+ if emit_thinking:
481
500
  body = (
482
501
  "<think>\n"
483
502
  + reasoning_content.strip("\n")
@@ -3,6 +3,14 @@
3
3
  Produces token-for-token identical output to tokenizer.apply_chat_template() while
4
4
  also tracking which message produced each token (for per-token loss masks).
5
5
 
6
+ One documented deviation: with ``enable_thinking=False`` the empty
7
+ ``<think>\n\n</think>\n\n`` wrapper is re-emitted on historical assistant
8
+ turns without ``reasoning_content`` (the Jinja template strips it from turns
9
+ at or before the last user query). The generation prompt prefills that
10
+ wrapper, so every turn sampled with thinking disabled contains it — stripping
11
+ it on re-render would make the same conversation produce different tokens
12
+ depending on when it is rendered. See ``tests/test_disabled_thinking_stability.py``.
13
+
6
14
  Multimodal: the Qwen3.5 family is itself a VLM (HF tag ``image-text-to-text``;
7
15
  processor class ``Qwen3VLProcessor``). When a user/tool message carries an
8
16
  ``ImagePart``, the renderer emits the same ``<|vision_start|>``+N×``<|image_pad|>``
@@ -959,9 +967,18 @@ class Qwen35Renderer:
959
967
  # call tags (``<function=...>``, ``<parameter=...>``, etc.) are
960
968
  # part of the model's emitted output too — keep them
961
969
  # ``is_content=True`` per the assistant rule.
962
- emit_thinking = self._should_render_thinking(
963
- msg_idx, last_query_index
964
- ) or getattr(self.config, "preserve_thinking", False)
970
+ # With thinking disabled, the generation prompt prefilled the empty
971
+ # ``<think>\n\n</think>\n\n`` wrapper, so it is part of every sampled
972
+ # turn's token stream. Re-emit it on historical turns — deviating
973
+ # from the Jinja template, which strips it from turns at or before
974
+ # the last user query — so re-renders stay token-stable with what
975
+ # the model actually sampled. Turns that carry reasoning_content
976
+ # were not sampled under this config; those stay template-faithful.
977
+ emit_thinking = (
978
+ self._should_render_thinking(msg_idx, last_query_index)
979
+ or getattr(self.config, "preserve_thinking", False)
980
+ or (not self.config.enable_thinking and not reasoning_content)
981
+ )
965
982
  if emit_thinking:
966
983
  # Include thinking block
967
984
  emit_special(self._think, msg_idx, is_sampled=True, is_content=True)
@@ -0,0 +1,226 @@
1
+ """Sampled-token stability with thinking disabled (Qwen family).
2
+
3
+ With ``enable_thinking=False`` the generation prompt prefills the empty
4
+ ``<think>\\n\\n</think>\\n\\n`` wrapper, so the wrapper is part of every
5
+ sampled turn's token stream. In an agentic rollout the conversation keeps
6
+ growing (tool responses, budget / prune-reminder user messages, ...) and may
7
+ be re-rendered from messages. The upstream Jinja template strips the wrapper
8
+ from assistant turns at or before the last user query, so a re-render would
9
+ diverge token-for-token from the stream the model actually sampled — forking
10
+ any token-identity comparison on byte-identical messages.
11
+
12
+ These tests pin the renderer-side guarantee that fixes this: with thinking
13
+ disabled, historical assistant turns without ``reasoning_content`` re-emit
14
+ the exact gen-prompt wrapper, so the tokens a turn was sampled with are a
15
+ prefix of every later re-render of the grown conversation, and the bridge
16
+ and a full re-render agree.
17
+
18
+ This is a deliberate, documented deviation from ``apply_chat_template`` —
19
+ see the carve-out in ``test_renderer_config_parity.py`` and the module
20
+ docstrings of ``renderers/qwen3.py`` / ``renderers/qwen35.py``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from functools import lru_cache
26
+
27
+
28
+ from renderers import create_renderer
29
+ from renderers.base import load_tokenizer
30
+ from renderers.configs import (
31
+ Qwen3RendererConfig,
32
+ Qwen35RendererConfig,
33
+ Qwen36RendererConfig,
34
+ )
35
+
36
+ # One representative model per affected renderer family, each with
37
+ # thinking explicitly disabled.
38
+ _MODELS = [
39
+ ("Qwen/Qwen3-8B", Qwen3RendererConfig(enable_thinking=False)),
40
+ ("Qwen/Qwen3.5-9B", Qwen35RendererConfig(enable_thinking=False)),
41
+ ("Qwen/Qwen3.6-35B-A3B", Qwen36RendererConfig(enable_thinking=False)),
42
+ ]
43
+
44
+ _EMPTY_WRAPPER = "<think>\n\n</think>\n\n"
45
+
46
+
47
+ @lru_cache(maxsize=None)
48
+ def _load(model_name: str):
49
+ return load_tokenizer(model_name)
50
+
51
+
52
+ def pytest_generate_tests(metafunc):
53
+ if "dt_model" in metafunc.fixturenames:
54
+ metafunc.parametrize("dt_model,dt_config", _MODELS, ids=[m for m, _ in _MODELS])
55
+
56
+
57
+ def test_sampled_stream_is_prefix_of_rerender(dt_model, dt_config):
58
+ """The rollout invariant: every generation prompt (which ends with the
59
+ prefilled empty wrapper) stays a byte prefix of all later re-renders of
60
+ the grown conversation — even after new real user queries arrive."""
61
+ tok = _load(dt_model)
62
+ renderer = create_renderer(tok, dt_config)
63
+
64
+ convo: list[dict] = [
65
+ {"role": "system", "content": "You are helpful."},
66
+ {"role": "user", "content": "What is the capital of France?"},
67
+ ]
68
+ prev = renderer.render_ids(convo, add_generation_prompt=True)
69
+ steps = [
70
+ (
71
+ {"role": "assistant", "content": "Paris."},
72
+ {"role": "user", "content": "You are running low on budget."},
73
+ ),
74
+ (
75
+ {"role": "assistant", "content": "Understood."},
76
+ {"role": "user", "content": "Submit your final answer now."},
77
+ ),
78
+ ]
79
+ for assistant, user in steps:
80
+ convo += [assistant, user]
81
+ cur = renderer.render_ids(convo, add_generation_prompt=True)
82
+ assert cur[: len(prev)] == prev, (
83
+ f"{dt_model}: re-render after appending {assistant['content']!r} "
84
+ "diverged from the earlier generation prompt — historical turn "
85
+ "lost its prefilled empty think wrapper"
86
+ )
87
+ prev = cur
88
+
89
+
90
+ def test_wrapper_reemitted_on_historical_turn(dt_model, dt_config):
91
+ """A historical assistant turn (before the last real user query, no
92
+ reasoning_content) renders with the exact empty wrapper bytes the
93
+ generation prompt prefilled."""
94
+ tok = _load(dt_model)
95
+ renderer = create_renderer(tok, dt_config)
96
+
97
+ rendered = renderer.render(
98
+ [
99
+ {"role": "user", "content": "What is 2+2?"},
100
+ {"role": "assistant", "content": "4"},
101
+ {"role": "user", "content": "Now 3+3?"},
102
+ ]
103
+ )
104
+ turn = [t for t, i in zip(rendered.token_ids, rendered.message_indices) if i == 1]
105
+ text = tok.decode(turn)
106
+ assert _EMPTY_WRAPPER in text, (
107
+ f"{dt_model}: historical assistant turn rendered without the "
108
+ f"prefilled empty think wrapper: {text!r}"
109
+ )
110
+
111
+
112
+ def test_bridge_and_rerender_agree(dt_model, dt_config):
113
+ """Extending a sampled turn via the bridge and re-rendering the same
114
+ conversation from messages must produce identical tokens — the property
115
+ whose violation shows up as token-level forks on byte-identical
116
+ trajectories."""
117
+ tok = _load(dt_model)
118
+ renderer = create_renderer(tok, dt_config)
119
+
120
+ msgs = [{"role": "user", "content": "What is the capital of France?"}]
121
+ prompt_ids = renderer.render_ids(msgs, add_generation_prompt=True)
122
+ # Simulate the sampled completion: content tokens then the stop token.
123
+ completion_ids = tok.encode("Paris.", add_special_tokens=False) + [
124
+ renderer.get_stop_token_ids()[0]
125
+ ]
126
+ reminder = {"role": "user", "content": "You are running low on budget."}
127
+
128
+ bridged = renderer.bridge_to_next_turn(prompt_ids, completion_ids, [reminder])
129
+ assert bridged is not None, (
130
+ f"{dt_model}: bridge refused with thinking disabled (implied "
131
+ "thinking_retention='all' should allow it)"
132
+ )
133
+
134
+ rerender = renderer.render_ids(
135
+ msgs + [{"role": "assistant", "content": "Paris."}, reminder],
136
+ add_generation_prompt=True,
137
+ )
138
+ assert bridged.token_ids == rerender, (
139
+ f"{dt_model}: bridge-extended tokens and full re-render disagree:\n"
140
+ f"bridge: {tok.decode(bridged.token_ids)!r}\n"
141
+ f"rerender: {tok.decode(rerender)!r}"
142
+ )
143
+
144
+
145
+ def test_tool_cycle_rerender_stays_prefix_stable(dt_model, dt_config):
146
+ """Tool-call turns (empty content, no reasoning) keep the prefilled
147
+ wrapper too. On qwen3 the template window additionally requires
148
+ ``is_last or reasoning_content``, so without the deviation a mid-cycle
149
+ re-render would strip the wrapper from the tool-call turn even though
150
+ it sits after the last real user query."""
151
+ tok = _load(dt_model)
152
+ renderer = create_renderer(tok, dt_config)
153
+ tools = [
154
+ {
155
+ "type": "function",
156
+ "function": {
157
+ "name": "get_weather",
158
+ "description": "Get the current weather for a city",
159
+ "parameters": {
160
+ "type": "object",
161
+ "properties": {"city": {"type": "string"}},
162
+ "required": ["city"],
163
+ },
164
+ },
165
+ }
166
+ ]
167
+
168
+ msgs: list[dict] = [{"role": "user", "content": "Weather in Paris?"}]
169
+ prev = renderer.render_ids(msgs, tools=tools, add_generation_prompt=True)
170
+
171
+ msgs += [
172
+ {
173
+ "role": "assistant",
174
+ "content": "",
175
+ "tool_calls": [
176
+ {"function": {"name": "get_weather", "arguments": {"city": "Paris"}}}
177
+ ],
178
+ },
179
+ {"role": "tool", "content": '{"temp": 20}'},
180
+ ]
181
+ cur = renderer.render_ids(msgs, tools=tools, add_generation_prompt=True)
182
+ assert cur[: len(prev)] == prev, (
183
+ f"{dt_model}: tool-call turn lost its prefilled empty think wrapper "
184
+ "on re-render"
185
+ )
186
+
187
+ msgs += [
188
+ {"role": "assistant", "content": "It is 20 degrees."},
189
+ {"role": "user", "content": "You are running low on budget."},
190
+ ]
191
+ cur2 = renderer.render_ids(msgs, tools=tools, add_generation_prompt=True)
192
+ assert cur2[: len(cur)] == cur
193
+
194
+
195
+ def test_historical_reasoning_stays_template_faithful(dt_model, dt_config):
196
+ """Scope guard: turns that carry ``reasoning_content`` were not sampled
197
+ under ``enable_thinking=False``, so they keep exact template parity —
198
+ the deviation fires only for the empty wrapper."""
199
+ tok = _load(dt_model)
200
+ renderer = create_renderer(tok, dt_config)
201
+
202
+ msgs = [
203
+ {"role": "user", "content": "What is 2+2?"},
204
+ {
205
+ "role": "assistant",
206
+ "reasoning_content": "Adding small ints.",
207
+ "content": "4",
208
+ },
209
+ {"role": "user", "content": "Now 3+3?"},
210
+ {
211
+ "role": "assistant",
212
+ "reasoning_content": "Same idea.",
213
+ "content": "6",
214
+ },
215
+ ]
216
+ ours = renderer.render_ids(msgs)
217
+ theirs = list(
218
+ tok.apply_chat_template(
219
+ msgs,
220
+ tokenize=True,
221
+ return_dict=False,
222
+ add_generation_prompt=False,
223
+ enable_thinking=False,
224
+ )
225
+ )
226
+ assert ours == theirs
@@ -437,6 +437,20 @@ def _build_tool_image_cases(make_part, image):
437
437
  # ---------------------------------------------------------------------------
438
438
 
439
439
 
440
+ def _skip_for_disabled_thinking_deviation(renderer, case_id) -> bool:
441
+ """With ``enable_thinking=False`` (the resolved default on the small
442
+ Qwen3.5 sizes) the Qwen family re-emits the empty think wrapper on
443
+ historical assistant turns without reasoning — a documented deviation
444
+ from the template for sampled-token stability (see
445
+ ``test_disabled_thinking_stability.py``). Cases with an assistant turn
446
+ before the last user query therefore diverge from the processor oracle
447
+ by exactly those wrapper tokens; skip them for parity purposes."""
448
+ return getattr(renderer.config, "enable_thinking", True) is False and case_id in (
449
+ "multi_turn_two_images",
450
+ "multi_turn_tool_response_images",
451
+ )
452
+
453
+
440
454
  def _supports_tool_message_images(renderer) -> bool:
441
455
  """True iff this renderer emits image placeholders inside tool-response
442
456
  content. Renderers without the feature silently drop image parts in tool
@@ -466,6 +480,8 @@ def test_multimodal_byte_parity_vs_processor(mm_model_name, modality, tiny_image
466
480
 
467
481
  for case in _build_cases(kit["make_part"], tiny_image):
468
482
  messages, add_gp = case.values
483
+ if _skip_for_disabled_thinking_deviation(renderer, case.id):
484
+ continue
469
485
 
470
486
  # Ours.
471
487
  ours = renderer.render_ids(messages, add_generation_prompt=add_gp)
@@ -709,6 +725,8 @@ def test_tool_response_image_byte_parity(mm_model_name, modality, tiny_image):
709
725
 
710
726
  for case in _build_tool_image_cases(kit["make_part"], tiny_image):
711
727
  messages, add_gp = case.values
728
+ if _skip_for_disabled_thinking_deviation(renderer, case.id):
729
+ continue
712
730
  ours = renderer.render_ids(messages, add_generation_prompt=add_gp)
713
731
  theirs = kit["processor_input_ids"](processor, messages, add_gp)
714
732
  assert ours == theirs, (
@@ -796,6 +814,8 @@ def test_add_vision_id_parity_vs_processor(
796
814
 
797
815
  for case in _build_cases(kit["make_part"], tiny_image):
798
816
  messages, add_gp = case.values
817
+ if _skip_for_disabled_thinking_deviation(renderer, case.id):
818
+ continue
799
819
  ours = renderer.render_ids(messages, add_generation_prompt=add_gp)
800
820
  theirs = _qwen_vl_processor_input_ids_with_kwargs(
801
821
  processor, messages, add_gp, add_vision_id=add_vision_id
@@ -194,3 +194,61 @@ def test_prime_qwen3_populates_assistant_masks(model):
194
194
  == len(rendered.is_content)
195
195
  == len(rendered.token_ids)
196
196
  )
197
+
198
+
199
+ @pytest.mark.parametrize("model", MODELS)
200
+ def test_prime_qwen3_empty_think_roundtrip(model):
201
+ """A sampled empty ``<think></think>`` block round-trips
202
+ parse → message → re-render byte-identically with the bridged sampled
203
+ stream. ``parse_qwen35`` used to collapse the empty-but-present block
204
+ to ``reasoning_content=None``; the renderer's key-presence check then
205
+ dropped the block on re-render, forking every later re-render of the
206
+ rollout (the template keeps reasoning on ALL historical turns, and
207
+ renders ``reasoning_content=""`` as ``<think></think>``)."""
208
+ tokenizer, renderer = _load(model)
209
+ stop = renderer.get_stop_token_ids()[0]
210
+
211
+ prompt = [{"role": "user", "content": "Reverse abc"}]
212
+ prompt_ids = renderer.render_ids(prompt, add_generation_prompt=True)
213
+ # Tokenize the sampled turn in context so the completion carries the
214
+ # exact BPE merges the model would produce continuing the stream.
215
+ sampled = "<think></think>\ncba"
216
+ full = tokenizer.encode(
217
+ tokenizer.decode(prompt_ids) + sampled, add_special_tokens=False
218
+ )
219
+ assert full[: len(prompt_ids)] == prompt_ids
220
+ completion_ids = full[len(prompt_ids) :] + [stop]
221
+
222
+ parsed = renderer.parse_response(completion_ids)
223
+ assert parsed.reasoning_content == "" # present-but-empty, not None
224
+ assert parsed.content == "cba"
225
+
226
+ assistant = {"role": "assistant", "content": parsed.content}
227
+ if parsed.reasoning_content is not None:
228
+ assistant["reasoning_content"] = parsed.reasoning_content
229
+ reminder = {"role": "user", "content": "Now reverse def"}
230
+
231
+ bridged = renderer.bridge_to_next_turn(prompt_ids, completion_ids, [reminder])
232
+ assert bridged is not None
233
+ rerender = renderer.render_ids(
234
+ [*prompt, assistant, reminder], add_generation_prompt=True
235
+ )
236
+ assert bridged.token_ids == rerender, (
237
+ f"{model}: bridged sampled stream and re-render disagree:\n"
238
+ f"bridge: {tokenizer.decode(bridged.token_ids)!r}\n"
239
+ f"rerender: {tokenizer.decode(rerender)!r}"
240
+ )
241
+
242
+
243
+ @pytest.mark.parametrize("model", MODELS)
244
+ def test_prime_qwen3_no_think_parses_to_none(model):
245
+ """Guard: a completion with no think block at all keeps
246
+ ``reasoning_content=None``, so the key stays absent from stored
247
+ messages and no ``<think></think>`` is invented on re-render."""
248
+ tokenizer, renderer = _load(model)
249
+ stop = renderer.get_stop_token_ids()[0]
250
+
251
+ completion_ids = tokenizer.encode("cba", add_special_tokens=False) + [stop]
252
+ parsed = renderer.parse_response(completion_ids)
253
+ assert parsed.reasoning_content is None
254
+ assert parsed.content == "cba"
@@ -418,6 +418,31 @@ def test_chat_template_kwarg_parity_hf(
418
418
  # config subclass that drops the field.
419
419
  assert kwarg in type(renderer.config).template_field_names()
420
420
 
421
+ # Documented deviation: with ``enable_thinking=False`` the Qwen family
422
+ # re-emits the empty ``<think>\n\n</think>\n\n`` wrapper on historical
423
+ # assistant turns without reasoning_content, where the Jinja template
424
+ # strips it. The generation prompt prefills the wrapper, so stripping
425
+ # it on re-render would make the sampled stream and the re-render of
426
+ # the same conversation disagree at the token level. ``multi_turn`` is
427
+ # the only shape with such a turn — except on qwen3, whose template
428
+ # window additionally requires ``is_last or reasoning_content``, so its
429
+ # non-last tool-call turn in ``tool_cycle`` is stripped too. Stability
430
+ # is pinned in ``test_disabled_thinking_stability.py``.
431
+ resolved = _resolve_renderer_name(model, renderer_name)
432
+ deviating_shapes = (
433
+ ("multi_turn", "tool_cycle") if resolved == "qwen3" else ("multi_turn",)
434
+ )
435
+ if (
436
+ resolved in ("qwen3", "qwen3.5", "qwen3.6")
437
+ and kwarg == "enable_thinking"
438
+ and value is False
439
+ and shape_id in deviating_shapes
440
+ ):
441
+ pytest.skip(
442
+ "deliberate template deviation: empty think wrapper kept on "
443
+ "historical turns for sampled-token stability"
444
+ )
445
+
421
446
  try:
422
447
  expected = _expected_hf(
423
448
  tokenizer, messages, kwarg=kwarg, value=value, **render_kwargs
@@ -9,7 +9,7 @@ resolution-markers = [
9
9
  ]
10
10
 
11
11
  [options]
12
- exclude-newer = "2026-06-19T02:36:32.208558271Z"
12
+ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
13
13
  exclude-newer-span = "P7D"
14
14
 
15
15
  [options.exclude-newer-package]
@@ -939,75 +939,54 @@ wheels = [
939
939
 
940
940
  [[package]]
941
941
  name = "pillow"
942
- version = "12.2.0"
943
- source = { registry = "https://pypi.org/simple" }
944
- sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
945
- wheels = [
946
- { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" },
947
- { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" },
948
- { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" },
949
- { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" },
950
- { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" },
951
- { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" },
952
- { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" },
953
- { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" },
954
- { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" },
955
- { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" },
956
- { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" },
957
- { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
958
- { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
959
- { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
960
- { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
961
- { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
962
- { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
963
- { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
964
- { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
965
- { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
966
- { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
967
- { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
968
- { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
969
- { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
970
- { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
971
- { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
972
- { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
973
- { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
974
- { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
975
- { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
976
- { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
977
- { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
978
- { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
979
- { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
980
- { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
981
- { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
982
- { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
983
- { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
984
- { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
985
- { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
986
- { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
987
- { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
988
- { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
989
- { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
990
- { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
991
- { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
992
- { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
993
- { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
994
- { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
995
- { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
996
- { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
997
- { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
998
- { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
999
- { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
1000
- { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
1001
- { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
1002
- { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
1003
- { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
1004
- { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
1005
- { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
1006
- { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
1007
- { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
1008
- { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
1009
- { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
1010
- { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
942
+ version = "12.3.0"
943
+ source = { registry = "https://pypi.org/simple" }
944
+ 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" }
945
+ wheels = [
946
+ { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" },
947
+ { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" },
948
+ { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" },
949
+ { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" },
950
+ { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" },
951
+ { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" },
952
+ { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" },
953
+ { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" },
954
+ { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" },
955
+ { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
956
+ { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
957
+ { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
958
+ { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
959
+ { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
960
+ { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
961
+ { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
962
+ { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
963
+ { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
964
+ { 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" },
965
+ { 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" },
966
+ { 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" },
967
+ { 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" },
968
+ { 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" },
969
+ { 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" },
970
+ { 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" },
971
+ { 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" },
972
+ { 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" },
973
+ { 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" },
974
+ { 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" },
975
+ { 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" },
976
+ { 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" },
977
+ { 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" },
978
+ { 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" },
979
+ { 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" },
980
+ { 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" },
981
+ { 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" },
982
+ { 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" },
983
+ { 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" },
984
+ { 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" },
985
+ { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
986
+ { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
987
+ { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
988
+ { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
989
+ { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
1011
990
  ]
1012
991
 
1013
992
  [[package]]
File without changes
File without changes