bazaar-compute-node 0.1.21__tar.gz → 0.1.22__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 (105) hide show
  1. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/attachments.py +18 -4
  5. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/attachments.py +76 -21
  6. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/LICENSE +0 -0
  7. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/README.md +0 -0
  8. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/__init__.py +0 -0
  9. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/__init__.py +0 -0
  10. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/agent.py +0 -0
  11. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/agent_management.py +0 -0
  12. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/application.py +0 -0
  13. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/command.py +0 -0
  14. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/config.py +0 -0
  15. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/registry.py +0 -0
  16. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
  17. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/system_service.py +0 -0
  18. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/transport.py +0 -0
  19. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  20. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/app/wrapper.py +0 -0
  21. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/bcc.py +0 -0
  22. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/cli.py +0 -0
  23. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  24. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
  25. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
  26. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
  27. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
  28. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
  29. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
  30. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
  31. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/codex/runtime.py +0 -0
  32. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/__init__.py +0 -0
  33. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/api.py +0 -0
  34. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/approval.py +0 -0
  35. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/channel.py +0 -0
  36. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/frame.py +0 -0
  37. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/identity.py +0 -0
  38. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/outbound.py +0 -0
  39. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/plugin.py +0 -0
  40. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/lark/transport.py +0 -0
  41. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  42. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  43. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  44. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  45. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +0 -0
  46. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  47. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  48. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  49. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  50. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
  51. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -0
  52. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
  53. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +0 -0
  54. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  55. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +0 -0
  56. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
  57. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
  58. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/approval.py +0 -0
  59. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
  60. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/channel.py +0 -0
  61. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
  62. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
  63. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/outbound.py +0 -0
  64. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/telegram/plugin.py +0 -0
  65. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  66. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  67. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  68. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
  69. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  70. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/__init__.py +0 -0
  71. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/approval.py +0 -0
  72. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/audit.py +0 -0
  73. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/channel.py +0 -0
  74. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/client.py +0 -0
  75. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/command.py +0 -0
  76. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/concurrency.py +0 -0
  77. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/correlation.py +0 -0
  78. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/instruction.py +0 -0
  79. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  80. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  81. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/entities.py +0 -0
  82. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/reminder.py +0 -0
  83. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/reminder_owner.py +0 -0
  84. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/models/states.py +0 -0
  85. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/observability.py +0 -0
  86. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  87. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  88. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/delivery.py +0 -0
  89. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/error_feedback.py +0 -0
  90. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/reminder.py +0 -0
  91. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
  92. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  93. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/session.py +0 -0
  94. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/orchestration/turn.py +0 -0
  95. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/outcomes.py +0 -0
  96. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/paths.py +0 -0
  97. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/reminder.py +0 -0
  98. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
  99. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/runtime.py +0 -0
  100. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/storage.py +0 -0
  101. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/core/timerwheel.py +0 -0
  102. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/__init__.py +0 -0
  103. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/catalog.py +0 -0
  104. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/english.py +0 -0
  105. {bazaar_compute_node-0.1.21 → bazaar_compute_node-0.1.22}/src/bazaar_compute_node/i18n/schinese.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: Runtime-agnostic computer node daemon for agents and channels.
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.21"
3
+ version = "0.1.22"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.21"
3
+ version = "0.1.22"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -88,10 +88,14 @@ class AttachmentMaterializer:
88
88
  size = 0
89
89
  descriptor: int | None = None
90
90
  try:
91
+ open_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
92
+ binary_flag = getattr(os, "O_BINARY", None)
93
+ if binary_flag is not None:
94
+ open_flags |= binary_flag
91
95
  descriptor = await asyncio.to_thread(
92
96
  os.open,
93
97
  temporary,
94
- os.O_WRONLY | os.O_CREAT | os.O_EXCL,
98
+ open_flags,
95
99
  0o600,
96
100
  )
97
101
  try:
@@ -117,6 +121,12 @@ class AttachmentMaterializer:
117
121
  raise ValueError("attachment workspace quota exceeded")
118
122
  await asyncio.to_thread(_write_all, descriptor, chunk)
119
123
  await asyncio.to_thread(os.fsync, descriptor)
124
+ stored_size = await asyncio.to_thread(os.fstat, descriptor)
125
+ if stored_size.st_size != size:
126
+ raise OSError(
127
+ "attachment write size mismatch: "
128
+ f"expected {size}, got {stored_size.st_size}"
129
+ )
120
130
  finally:
121
131
  await asyncio.to_thread(os.close, descriptor)
122
132
  descriptor = None
@@ -165,7 +175,8 @@ class AttachmentMaterializer:
165
175
  return 0
166
176
  return sum(
167
177
  path.stat().st_size
168
- for path in root.glob("*/content.*")
178
+ for pattern in ("*/content", "*/content.*")
179
+ for path in root.glob(pattern)
169
180
  if path.is_file() and not path.is_symlink()
170
181
  )
171
182
 
@@ -183,8 +194,11 @@ def _attachment_suffix(name: str, media_type: str | None) -> str:
183
194
  suffix = Path(name).suffix
184
195
  if _SAFE_SUFFIX.fullmatch(suffix):
185
196
  return suffix
186
- guessed = mimetypes.guess_extension(media_type or "") or ".bin"
187
- return guessed if _SAFE_SUFFIX.fullmatch(guessed) else ".bin"
197
+ media_type = (media_type or "").partition(";")[0].strip().lower()
198
+ if not media_type or media_type.endswith("/octet-stream"):
199
+ return ""
200
+ guessed = mimetypes.guess_extension(media_type) or ""
201
+ return guessed if _SAFE_SUFFIX.fullmatch(guessed) else ""
188
202
 
189
203
 
190
204
  __all__ = ["AttachmentMaterializer"]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
+ import mimetypes
5
6
  from collections import OrderedDict
6
7
  from collections.abc import AsyncIterable, AsyncIterator, Mapping
7
8
  from contextlib import suppress
@@ -248,16 +249,22 @@ class LarkResourceCache:
248
249
  ):
249
250
  raise ValueError("resource_too_large")
250
251
  name = _safe_filename(
251
- _content_disposition_filename(
252
- response.headers.get("Content-Disposition")
253
- ),
254
- fallback=name,
252
+ name,
253
+ fallback=resource.resource_type,
255
254
  resource_type=resource.resource_type,
256
255
  )
257
256
  media_type = _safe_media_type(
258
257
  response.headers.get("Content-Type"),
259
258
  fallback=media_type,
260
259
  )
260
+ name = _resolve_resource_name(
261
+ event_name=name,
262
+ response_name=_content_disposition_filename(
263
+ response.headers.get("Content-Disposition")
264
+ ),
265
+ media_type=media_type,
266
+ resource_type=resource.resource_type,
267
+ )
261
268
  return await self._materializer.materialize(
262
269
  _bounded_stream(
263
270
  response.content.iter_chunked(_RESOURCE_CHUNK_BYTES)
@@ -475,7 +482,7 @@ def _resource_from_mapping(
475
482
  break
476
483
  name = _safe_filename(
477
484
  raw_name,
478
- fallback=f"{resource_type}.bin",
485
+ fallback=resource_type,
479
486
  resource_type=resource_type,
480
487
  )
481
488
  media_type = _safe_media_type(mapping.get("mime_type"))
@@ -550,23 +557,71 @@ def _safe_filename(
550
557
  resource_type: str,
551
558
  ) -> str:
552
559
  candidate = value if isinstance(value, str) and value else fallback
553
- if (
554
- not candidate
555
- or len(candidate) > 255
556
- or "/" in candidate
557
- or "\\" in candidate
558
- or any(ord(character) < 32 or ord(character) == 127 for character in candidate)
559
- ):
560
+ if not _is_safe_filename(candidate):
560
561
  candidate = fallback
561
- if (
562
- not candidate
563
- or len(candidate) > 255
564
- or "/" in candidate
565
- or "\\" in candidate
566
- or any(ord(character) < 32 or ord(character) == 127 for character in candidate)
567
- ):
568
- return f"{resource_type}.bin"
569
- return candidate
562
+ return candidate if _is_safe_filename(candidate) else resource_type
563
+
564
+
565
+ def _resolve_resource_name(
566
+ *,
567
+ event_name: str,
568
+ response_name: str | None,
569
+ media_type: str | None,
570
+ resource_type: str,
571
+ ) -> str:
572
+ response_name = response_name if _is_safe_filename(response_name) else None
573
+ if _safe_suffix(event_name):
574
+ name = event_name
575
+ elif response_name is not None and _safe_suffix(response_name):
576
+ name = response_name
577
+ elif event_name != resource_type:
578
+ name = event_name
579
+ elif response_name is not None:
580
+ name = response_name
581
+ else:
582
+ name = event_name
583
+
584
+ if _safe_suffix(name):
585
+ return name
586
+ suffix = _suffix_from_media_type(media_type)
587
+ if not suffix and resource_type == "audio":
588
+ suffix = ".mp3"
589
+ return f"{name}{suffix}" if suffix else name
590
+
591
+
592
+ def _is_safe_filename(value: object) -> bool:
593
+ return (
594
+ isinstance(value, str)
595
+ and bool(value)
596
+ and len(value) <= 255
597
+ and "/" not in value
598
+ and "\\" not in value
599
+ and not any(ord(character) < 32 or ord(character) == 127 for character in value)
600
+ )
601
+
602
+
603
+ def _safe_suffix(name: str) -> str:
604
+ dot = name.rfind(".")
605
+ if dot <= 0:
606
+ return ""
607
+ suffix = name[dot:]
608
+ return (
609
+ suffix
610
+ if 1 < len(suffix) <= 11 and suffix[1:].isascii() and suffix[1:].isalnum()
611
+ else ""
612
+ )
613
+
614
+
615
+ def _suffix_from_media_type(media_type: str | None) -> str:
616
+ media_type = (media_type or "").partition(";")[0].strip().lower()
617
+ if not media_type or media_type.endswith("/octet-stream"):
618
+ return ""
619
+ suffix = mimetypes.guess_extension(media_type) or ""
620
+ return (
621
+ suffix
622
+ if 1 < len(suffix) <= 11 and suffix[1:].isascii() and suffix[1:].isalnum()
623
+ else ""
624
+ )
570
625
 
571
626
 
572
627
  def _safe_media_type(value: object, *, fallback: str | None = None) -> str | None: