Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine c39e801ad6 🎨(frontend) apply lint fixes
Run the frontend linter/formatter and commit the resulting fixes to
keep the codebase clean and consistent.
2026-08-04 20:26:44 +02:00
83 changed files with 362 additions and 3347 deletions
-10
View File
@@ -17,9 +17,6 @@ and this project adheres to
- ✨(backend) add roomkit viewset to start a room without WebRTC join
- ✨(frontend) let users set default configuration for generated links
- ✨(frontend) expose media state to external gateways
- ✨(frontend) add connection test feature
- ✨(sdk) allow passing a background color to the calendar iframe
- ✨(sdk) add a room configuration popup from CreateMeetingButton
### Changed
@@ -33,7 +30,6 @@ and this project adheres to
- 💄(frontend) render Avatar initials in uppercase
- 💄(frontend) improve participant name rendering in the list
- 🚚(backend) rename TelephonyService to SIPManagement
- ⬆️(dependencies) update python dependencies
### Fixed
@@ -45,12 +41,6 @@ and this project adheres to
- 🐛(frontend) fall back to user.full_name on request-entry
- 🚸(frontend) show two initials in the Avatar when possible
- 🩹(all) clear the SonarCloud reliability finding and the lint debt
- 🐛(frontend) stop the installed app reopening the room it came from
- 🐛(backend) serialize lazy title in summary payload
- 💄(frontend) show pointer cursor on interactive switches
- 🐛(frontend) fix icon centering in the Switch primitive
- 🐛(frontend) keep Unicode initials intact in avatar
- 🐛(frontend) prevent concurrent settings updates from overwriting each other
## [1.24.0] - 2026-07-21
-3
View File
@@ -104,6 +104,3 @@ APPLICATION_JWT_AUDIENCE=http://localhost:8071/external-api/v1.0/
APPLICATION_JWT_SECRET_KEY=devKey
APPLICATION_BASE_URL=http://localhost:3000
# Diagnostics
CONNECTION_TEST_ENABLED = True
+5 -5
View File
@@ -4,19 +4,19 @@ name = "agents"
version = "1.24.0"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.6.7",
"livekit-plugins-deepgram==1.6.7",
"livekit-plugins-silero==1.6.7",
"livekit-agents==1.6.4",
"livekit-plugins-deepgram==1.6.4",
"livekit-plugins-silero==1.6.4",
"livekit-plugins-kyutai-lasuite==0.0.6",
"python-dotenv==1.2.2",
"protobuf==6.33.6",
"minio==7.2.20",
"sentry-sdk==2.66.1",
"sentry-sdk==2.60.0",
]
[project.optional-dependencies]
dev = [
"ruff==0.16.0",
"ruff==0.15.19",
]
[tool.uv]
+52 -52
View File
@@ -29,15 +29,15 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "livekit-agents", specifier = "==1.6.7" },
{ name = "livekit-plugins-deepgram", specifier = "==1.6.7" },
{ name = "livekit-agents", specifier = "==1.6.4" },
{ name = "livekit-plugins-deepgram", specifier = "==1.6.4" },
{ name = "livekit-plugins-kyutai-lasuite", specifier = "==0.0.6" },
{ name = "livekit-plugins-silero", specifier = "==1.6.7" },
{ name = "livekit-plugins-silero", specifier = "==1.6.4" },
{ name = "minio", specifier = "==7.2.20" },
{ name = "protobuf", specifier = "==6.33.6" },
{ name = "python-dotenv", specifier = "==1.2.2" },
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.0" },
{ name = "sentry-sdk", specifier = "==2.66.1" },
{ name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.19" },
{ name = "sentry-sdk", specifier = "==2.60.0" },
]
provides-extras = ["dev"]
@@ -762,16 +762,16 @@ wheels = [
[[package]]
name = "json-repair"
version = "0.60.1"
version = "0.59.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/a6/d69888cb4ffde30e80db1e6c32caaadd2f984a80067d5ea72c2cb3f61c3f/json_repair-0.60.1.tar.gz", hash = "sha256:841661cdd2df507c9a4e189097f38ca6bc372e06d4b4e36d72e590f68176c290", size = 49451, upload-time = "2026-06-03T17:28:44.451Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/7c/e95bb03068572146eba37e8175c760f470ea0a6097310e16bbf2bc6e6457/json_repair-0.59.10.tar.gz", hash = "sha256:2e4b85537c752d8a513ea28fdad891e5ede32c83de745366b97f648b8c34ede7", size = 49133, upload-time = "2026-05-14T06:41:51.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/1f/2a2b5eea8ef5762a86ad3f8fddddaaba2c0d76dd44e644b9158900868bec/json_repair-0.60.1-py3-none-any.whl", hash = "sha256:ba6ff974f2a8bef2f7768144a7f03f870a816443f03da27a49cdd0ec31a78049", size = 48045, upload-time = "2026-06-03T17:28:43.038Z" },
{ url = "https://files.pythonhosted.org/packages/ee/87/49b20c6b81493d55c311f711ed87319d0fbad8bd0bbfbe36e52103af36bd/json_repair-0.59.10-py3-none-any.whl", hash = "sha256:5468fa3eaadcc9b4a5646776bc4176e2fe5f374b5848a15f468cce3b60e3db0e", size = 47742, upload-time = "2026-05-14T06:41:49.812Z" },
]
[[package]]
name = "livekit"
version = "1.1.13"
version = "1.1.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -779,18 +779,18 @@ dependencies = [
{ name = "protobuf" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/92/dcd4f295913533ddd0d48153cc28f1358d550bea651460bd895256981c4d/livekit-1.1.13.tar.gz", hash = "sha256:aa2bd89cf0c2ebcaa71a240275964c23900a0422a2a3d43d274e88a211a0ecfc", size = 370211, upload-time = "2026-06-30T11:54:00.321Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/2c/3e8412615a2f4b9abdd1dec54138f8810b58e1e5c366443c098d52ef1957/livekit-1.1.12.tar.gz", hash = "sha256:a8e3aa59a7299b136a2a5442f90a9c8a5a8188afe94ffeeb85873d6ceaabf939", size = 369150, upload-time = "2026-06-24T19:50:49.651Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/bd/15100217109595aedbb9bcfdfc1c77513c0f44940d72644d16b611476941/livekit-1.1.13-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2a19b023de9a573fe5da629e3ee514f2962c87e1afec95a6b19bbbdb6ea8f703", size = 10147642, upload-time = "2026-06-30T11:53:48.781Z" },
{ url = "https://files.pythonhosted.org/packages/97/dd/4f001a9c5ccde361a53437a09bc04dc9cad8003c6711c2bcc0734e18e626/livekit-1.1.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:90b6796e0c4515bc1e8a1e109a40b88d82c36b12452b7ae01fe35be7ee8357ba", size = 8968740, upload-time = "2026-06-30T11:53:51.223Z" },
{ url = "https://files.pythonhosted.org/packages/26/1a/7e97a45a4b6e10ce3a4e9938e3d3fa0a6512a6557f5990685eab4f6e88d1/livekit-1.1.13-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:43779c0f3bb27589cd517d60c442c674c2f967a97db158829a533974a29a3997", size = 9980507, upload-time = "2026-06-30T11:53:53.349Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9d/389bbdf39ccd2c464a4749ba7be1584dea608518c32a5ddbc511db6b0cf7/livekit-1.1.13-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f4e83f0e272f4b2e9cc39dbefd7bb23b75bd8c105478d867c2869254ca4e149a", size = 11367692, upload-time = "2026-06-30T11:53:55.409Z" },
{ url = "https://files.pythonhosted.org/packages/da/ce/a3d3e0566dbd2586c325240d44afec6d44421eb794bcf8dbaca15463a7b7/livekit-1.1.13-py3-none-win_amd64.whl", hash = "sha256:22dff7a39cb3d590a4757e20d3ce5d326ab882350386f5070f45cbd6d9ccf839", size = 10717013, upload-time = "2026-06-30T11:53:57.701Z" },
{ url = "https://files.pythonhosted.org/packages/39/6f/b2a1486f9f217043dbf52ed93bf6a77febd4d86615b02cddd7758a225f52/livekit-1.1.12-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:7fe82d7f2ef76ef6f7d856581f4519c2870fcffe3c33a8b2e74315d7c0bbdadc", size = 10140961, upload-time = "2026-06-24T19:50:38.69Z" },
{ url = "https://files.pythonhosted.org/packages/fb/de/656428ad72ce5b6911be2f084ead1e9c3b04d46015981de7e94177628a8d/livekit-1.1.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3c33d6b6df872447d3295443eb4886b84b6d6045ce8b3627504ee840ddf908fb", size = 8965085, upload-time = "2026-06-24T19:50:40.945Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ce/572f3f15571625ee9f99f103f2a13503797ed80636d70ce0dd58034e5e1d/livekit-1.1.12-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0f452415ec4c7c789bd99bc098dc19710f49249993b13335e5741de50fa423dc", size = 9974856, upload-time = "2026-06-24T19:50:43.329Z" },
{ url = "https://files.pythonhosted.org/packages/db/d7/5a798f8ef40889c8236a05d1a3985a77114c2c8f4584ac118987d6f8d9d5/livekit-1.1.12-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:19611fb5fa6bd1e6366acc7526aecab1c8fa2afe9e579f73d34df575c667aa53", size = 11359455, upload-time = "2026-06-24T19:50:45.654Z" },
{ url = "https://files.pythonhosted.org/packages/1d/45/f28e25888babc83a43769f428545c9ee781366a0ef22b4b479318c0f95bc/livekit-1.1.12-py3-none-win_amd64.whl", hash = "sha256:4e81a366a6c7a83b435d9de15760da70d4c13748b61eb8d5f000c9d279c8a39e", size = 10710076, upload-time = "2026-06-24T19:50:47.663Z" },
]
[[package]]
name = "livekit-agents"
version = "1.6.7"
version = "1.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiofiles" },
@@ -825,9 +825,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "watchfiles" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/6d/a1cccc1fa97dd4f4a76bd92d03fe0742607f6a61bd010867f5714a73d5a3/livekit_agents-1.6.7.tar.gz", hash = "sha256:039112aa05cea17328c3d7bbb69f164e75e35e8408480f3479c2c356e4542a17", size = 2626576, upload-time = "2026-07-25T02:04:04.812Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/a1/e681926fd3ddd3323a50b638e5c9f8d5cea68db0eaafcc040dbf65b5efeb/livekit_agents-1.6.4.tar.gz", hash = "sha256:deb1b47a1ab637c93ab675980bb4532fb01244604b41eda9d6e3ca268a52e794", size = 2561744, upload-time = "2026-06-24T20:49:24.032Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9b/46c123ae94e36cd68e1327ce5b9aa378fa2d44916e549ccfb674e795a8e8/livekit_agents-1.6.7-py3-none-any.whl", hash = "sha256:f517fd5d559a48cd776bd13fd955b26889f2666dd9a07d233e1e5225e409563d", size = 2738751, upload-time = "2026-07-25T02:04:02.52Z" },
{ url = "https://files.pythonhosted.org/packages/e4/bb/c50992829fadd273fed66ae761f87ed4296fdd37eba0d0bc8c79cdddd896/livekit_agents-1.6.4-py3-none-any.whl", hash = "sha256:5341849645f768ed8bd1d276688f2b6ea0e72574e2aab7206d4e18c5628a97cd", size = 2669501, upload-time = "2026-06-24T20:49:22.119Z" },
]
[package.optional-dependencies]
@@ -837,7 +837,7 @@ codecs = [
[[package]]
name = "livekit-api"
version = "1.2.0"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -846,9 +846,9 @@ dependencies = [
{ name = "pyjwt" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/19/36ff6712ec638a4b7dad4d8f03795952e401dc31db0b04cddec7892650da/livekit_api-1.2.0.tar.gz", hash = "sha256:a89817b3bca9584873786ff07209839308217537a42f95ecb2609aafaa109ddc", size = 20778, upload-time = "2026-07-11T23:20:54.781Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/03/00e0ec173f247e1f7ea63cb5591d5680a64c7a74ea4d5d558e5aed6cc399/livekit_api-1.1.1.tar.gz", hash = "sha256:70c7b80eecbc297b40756ebd76e4f52d00b0348fb7d212a21c1f69cc57fd9c83", size = 15196, upload-time = "2026-06-24T01:36:19.686Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/e7/8926f16d4bc1b2e0ae46d4a507321bb899396d263a757f1adaabcd3b3867/livekit_api-1.2.0-py3-none-any.whl", hash = "sha256:307f8e5cfb0358c3ca091814ab768af55896022151bcd7f951954ccefa036a24", size = 26499, upload-time = "2026-07-11T23:20:53.736Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c0/d5f3ff74ab5db2d06f173801ec934885d11a754b9fb9ad768c8ede0a6c89/livekit_api-1.1.1-py3-none-any.whl", hash = "sha256:ce8c327676c366e66cf68782934368dd0ba92b9d48f578275227e255c890fe88", size = 19471, upload-time = "2026-06-24T01:36:18.42Z" },
]
[[package]]
@@ -902,15 +902,15 @@ wheels = [
[[package]]
name = "livekit-plugins-deepgram"
version = "1.6.7"
version = "1.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "livekit-agents", extra = ["codecs"] },
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/0b/32b40c498d76d23283e02df8410b5adba21919b8d6adac8e88e83c8d5bad/livekit_plugins_deepgram-1.6.7.tar.gz", hash = "sha256:76f102d69c5159d87aa53581b8402aa28838d512203ccbc79212a3edda0646b0", size = 22610, upload-time = "2026-07-25T02:04:38.185Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3e/75/f61c70b7bb85f2f246ef438acd6f3f240de700fe59d8d53bdb0c7898886f/livekit_plugins_deepgram-1.6.4.tar.gz", hash = "sha256:d01292efcd0dd3875ba87d652efe14e6e3c5d26a327d8e8e86a1983bbe94b7e3", size = 18347, upload-time = "2026-06-24T20:49:45.321Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/f9/f9e25f08321c24f85cdb00e75163c6b534d254dec7755e8481f0ecc6e270/livekit_plugins_deepgram-1.6.7-py3-none-any.whl", hash = "sha256:741ceb59ae181b5eeb9f95bd5dec0c8381e3f7532a10d67c7ab1fcf024085643", size = 26084, upload-time = "2026-07-25T02:04:36.935Z" },
{ url = "https://files.pythonhosted.org/packages/2d/00/c04d24daed22fee9f3cec3cea6f9e5e0217fd9c1df725c585ac08a42c3c4/livekit_plugins_deepgram-1.6.4-py3-none-any.whl", hash = "sha256:a590f2251d5ccda4a555077cbfccbb66194b6ec55911e7bd33d89565eba15611", size = 23084, upload-time = "2026-06-24T20:49:44.126Z" },
]
[[package]]
@@ -929,29 +929,29 @@ wheels = [
[[package]]
name = "livekit-plugins-silero"
version = "1.6.7"
version = "1.6.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "livekit-agents" },
{ name = "numpy" },
{ name = "onnxruntime" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/6a/213524075717b84d140efec953fe46ec7c6ad408014f7d20bee48195e4f6/livekit_plugins_silero-1.6.7.tar.gz", hash = "sha256:481503ece53c44bd36bbfdea0f97d067bd4dc2ef1c04f9a0244c3397dd9d1966", size = 1955926, upload-time = "2026-07-25T02:06:27.383Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fa/85/ae6e73640ade39968d64bf0737235a5ffe11e78948a061ddf7b7f4dbf894/livekit_plugins_silero-1.6.4.tar.gz", hash = "sha256:4a9bdf6d3ccb1c0433fd9c39ae7174f7275d4de183acb59a60ff8edb62fcaef0", size = 1956517, upload-time = "2026-06-24T20:51:20.554Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/9f/0622854a2a99a6a4f08a2e13b59f000f16ab8ccddf897d223b90b2599bd2/livekit_plugins_silero-1.6.7-py3-none-any.whl", hash = "sha256:ab56544919c2046b8fe6c58f688fe74961d4e1eb273ea7e7912bb4917b315d21", size = 3903914, upload-time = "2026-07-25T02:06:25.908Z" },
{ url = "https://files.pythonhosted.org/packages/e3/00/3f53f00632fc07767d4c367815096c3cf80e4c3af28da8a1e29866b6843c/livekit_plugins_silero-1.6.4-py3-none-any.whl", hash = "sha256:b613f93c5aa4c7635ea12691795446ca2bdff6499417b42c7de8b14cc1df7a6f", size = 3904448, upload-time = "2026-06-24T20:51:18.615Z" },
]
[[package]]
name = "livekit-protocol"
version = "1.1.21"
version = "1.1.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/ae/9d60fe37d85623e68a2e36ae31d18c671949db67d2a13a6438410d930466/livekit_protocol-1.1.21.tar.gz", hash = "sha256:8bb1ac1aba5d37d0af43e9d56d129a5d16295cbd91518b00fd157e258f20a6ef", size = 122363, upload-time = "2026-07-21T18:28:26.372Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/88/64f2be01a630e249f1dbd0d51876f109b53b7899ae41246d2ca5b647086d/livekit_protocol-1.1.18.tar.gz", hash = "sha256:187af32ebf75333a62117b0db9e551c99060bd4e1f57cfc0fce73bcd7a671da8", size = 115802, upload-time = "2026-06-27T15:31:04.102Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/d2/ec10b1cdf912235c2b07898be6ba2535e50f23e8980bb719f29decbd8034/livekit_protocol-1.1.21-py3-none-any.whl", hash = "sha256:ce0bb763327c91349ee8831843c4f8bd72132c4d06ac572171f2ae5c0217211d", size = 149245, upload-time = "2026-07-21T18:28:24.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/80/9cc33e4d0280132538850aaf9559d6b8aa9e670c5917f75dab996400ab84/livekit_protocol-1.1.18-py3-none-any.whl", hash = "sha256:30c539410fd3cfc2e551ca3a193aaaaacaaec6dd57dabe2c9be7c7c7d15f0e01", size = 143134, upload-time = "2026-06-27T15:31:02.686Z" },
]
[[package]]
@@ -1753,40 +1753,40 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" },
{ url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" },
{ url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" },
{ url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" },
{ url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" },
{ url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" },
{ url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" },
{ url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" },
{ url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" },
{ url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" },
{ url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" },
{ url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" },
{ url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" },
{ url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" },
{ url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" },
{ url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" },
]
[[package]]
name = "sentry-sdk"
version = "2.66.1"
version = "2.60.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" }
sdist = { url = "https://files.pythonhosted.org/packages/54/a2/2e6c090db384cc515069f4f85542bd5baf6786852073020ea73d4a76d3ea/sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978", size = 452946, upload-time = "2026-05-13T13:34:52.516Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" },
{ url = "https://files.pythonhosted.org/packages/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" },
]
[[package]]
-1
View File
@@ -65,7 +65,6 @@ def get_frontend_configuration(request):
"default_access_level": settings.RESOURCE_DEFAULT_ACCESS_LEVEL,
},
"subtitle": {"enabled": settings.ROOM_SUBTITLE_ENABLED},
"diagnostics": {"connection_test_enabled": settings.CONNECTION_TEST_ENABLED},
"livekit": {
"url": settings.LIVEKIT_CONFIGURATION["url"],
"force_wss_protocol": settings.LIVEKIT_FORCE_WSS_PROTOCOL,
-1
View File
@@ -17,7 +17,6 @@ class FeatureFlag:
"addons": "ADDONS_ENABLED",
"application": "APPLICATION_ENABLED",
"roomkit": "ROOMKIT_ENABLED",
"connection_test": "CONNECTION_TEST_ENABLED",
}
@classmethod
-34
View File
@@ -198,37 +198,3 @@ class IsPresentInMeeting(permissions.BasePermission):
return False
except ParticipantsManagementException:
return False
class RoomCapabilityPermission(permissions.BasePermission):
"""Wip."""
capability = None
def has_object_permission(self, request, view, obj):
"""Wip."""
user = request.user
if not user or not user.is_authenticated:
return False
if obj.is_administrator_or_owner(user):
return True
if not obj.configuration.get(self.capability, True):
return False
try:
return ParticipantsManagement().check_if_in_meeting(
room_name=str(obj.pk), identity=str(user.sub)
)
except ParticipantNotFoundException:
return False
except ParticipantsManagementException:
return False
class CanRecord(RoomCapabilityPermission):
"""Wip."""
capability = "authenticated_can_record"
-1
View File
@@ -349,7 +349,6 @@ class RoomConfiguration(BaseModel):
can_publish_sources: list[TrackSource] | None = None
everyone_can_mute: bool | None = None
authenticated_can_record: bool | None = None
model_config = {"extra": "forbid"}
-12
View File
@@ -85,15 +85,3 @@ class RoomKitJoinRateThrottle(MonitoredUserRateThrottle):
"""
scope = "roomkit_join"
class ConnectionTestUserRateThrottle(MonitoredUserRateThrottle):
"""Throttle authenticated users requesting connection test tokens."""
scope = "connection_test"
class ConnectionTestAnonRateThrottle(MonitoredAnonRateThrottle):
"""Throttle anonymous users requesting connection test tokens."""
scope = "connection_test"
+7 -99
View File
@@ -2,10 +2,8 @@
# pylint: disable=too-many-lines
import uuid
from datetime import timedelta
from logging import getLogger
from urllib.parse import unquote, urlparse
from uuid import uuid4
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
@@ -29,9 +27,6 @@ from rest_framework import (
from rest_framework import (
exceptions as drf_exceptions,
)
from rest_framework import (
permissions as drf_permissions,
)
from rest_framework import (
response as drf_response,
)
@@ -41,7 +36,6 @@ from rest_framework import (
from rest_framework.settings import api_settings
from core import analytics, enums, models, utils
from core.api import throttling
from core.api.filters import ListFileFilter
from core.enums import MEDIA_STORAGE_URL_PATTERN
from core.recording.enums import FileExtension
@@ -99,9 +93,7 @@ from core.services.room_roles import (
RoomRoleService,
)
from core.services.subtitle import SubtitleException, SubtitleService
from core.tasks.connection_test import delete_connection_test_room
from core.tasks.file import process_file_deletion
from core.utils import generate_token
from ..authentication.livekit import LiveKitTokenAuthentication
from ..models import RoomAccessLevel
@@ -396,7 +388,7 @@ class RoomViewSet(
methods=["post"],
url_path="start-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
@@ -421,30 +413,11 @@ class RoomViewSet(
mode=mode,
options=options.model_dump(exclude_none=True) if options else {},
)
if room.is_administrator_or_owner(request.user):
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
else:
# todo - encapsulate it in a clear method which replicate privileges role on obj
accesses = models.ResourceAccess.objects.filter(
resource=room,
role__in=[models.RoleChoices.OWNER, models.RoleChoices.ADMIN],
).values_list("user_id", "role")
models.RecordingAccess.objects.bulk_create(
[
models.RecordingAccess(
user_id=user_id,
role=role,
recording=recording,
)
for user_id, role in accesses
]
)
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
except (DjangoValidationError, IntegrityError):
# DjangoValidationError covers the Python-level check (full_clean);
@@ -488,7 +461,7 @@ class RoomViewSet(
methods=["post"],
url_path="stop-recording",
permission_classes=[
permissions.CanRecord,
permissions.HasPrivilegesOnRoom,
],
)
@FeatureFlag.require("recording")
@@ -1590,68 +1563,3 @@ class FileViewSet(
request = utils.generate_s3_authorization_headers(f"{url_params.get('key'):s}")
return drf_response.Response("authorized", headers=request.headers, status=200)
class DiagnosticsViewSet(viewsets.ViewSet):
"""Endpoints helping users and support diagnose connectivity issues.
Diagnostics are grouped behind a single prefix so upcoming checks
(rtcstats collection, ICE candidate reports, etc.) can be added as new
actions rather than new top-level routes.
They are open to anonymous users: someone who cannot join a room is
exactly who needs to run a test, and they may well not be logged in.
Each action therefore carries its own throttle scope.
"""
permission_classes = [drf_permissions.AllowAny]
@decorators.action(
detail=False,
methods=["POST"],
url_path="connection",
url_name="connection",
throttle_classes=[
throttling.ConnectionTestUserRateThrottle,
throttling.ConnectionTestAnonRateThrottle,
],
)
@FeatureFlag.require("connection_test")
def connection(self, request):
"""Return a short-lived LiveKit token for an ephemeral test room.
Going through the room API is not an option here: it is tied to
registered meetings, lobby rules and longer-lived tokens. Each call
gets its own room so two people testing at the same time never meet.
"""
room = f"{settings.CONNECTION_TEST_ROOM_PREFIX}-{uuid4()}"
expires_in = settings.CONNECTION_TEST_TOKEN_TTL_SECONDS
# LiveKit refreshes tokens for connected clients, so JWT TTL alone does not
# eject someone who stays connected. Schedule a hard DeleteRoom when Celery
# is available.
if settings.CELERY_ENABLED:
max_age = (
settings.CONNECTION_TEST_TOKEN_TTL_SECONDS
+ settings.CONNECTION_TEST_ROOM_EXTRA_AGE_SECONDS
)
delete_connection_test_room.apply_async(
args=[room],
countdown=max_age,
)
return drf_response.Response(
{
"livekit": {
"url": settings.LIVEKIT_CONFIGURATION["url"],
"room": room,
"token": generate_token(
room=room,
user=request.user,
username="Connection Test",
ttl=timedelta(seconds=expires_in),
),
"expires_in": expires_in,
},
}
)
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class BaseJWTAuthentication(authentication.BaseAuthentication):
"""Base JWT authentication class."""
def __init__( # noqa: PLR0917
def __init__(
self,
secret_key,
algorithm,
@@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.translation import get_language, gettext, override
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
import aiohttp
@@ -121,7 +121,7 @@ class NotificationService:
msg_plain = render_to_string(
"mail/text/screen_recording.txt", personalized_context
)
subject = gettext("Your recording is ready") # Force translation
subject = str(_("Your recording is ready")) # Force translation
try:
send_mail(
@@ -192,7 +192,7 @@ class NotificationService:
"""Generate title from context or return default."""
if recording_datetime is None:
with override(locale):
return gettext("Transcription")
return _("Transcription")
dt = recording_datetime
if owner_timezone:
+1 -1
View File
@@ -30,7 +30,7 @@ class TokenDecodeError(JWTError):
class JwtTokenService:
"""Generic JWT token service with configurable settings."""
def __init__( # noqa: PLR0917
def __init__(
self,
secret_key: str,
algorithm: str,
@@ -137,13 +137,6 @@ class LiveKitEventsService:
room_name = data.room.name or data.egress_info.room_name
if self._is_connection_test_room(room_name):
logger.info(
"Ignoring webhook event for connection test room '%s'.",
room_name,
)
return
if self._filter_regex and not self._filter_regex.search(room_name):
logger.info("Filtered webhook event for room '%s'", room_name)
return
@@ -235,11 +228,6 @@ class LiveKitEventsService:
# Silently ignoring EGRESS_ABORTED, EGRESS_FAILED
@staticmethod
def _is_connection_test_room(room_name: str) -> bool:
"""Return True for ephemeral rooms created by the connection test endpoint."""
return room_name.startswith(settings.CONNECTION_TEST_ROOM_PREFIX)
def _handle_room_started(self, data):
"""Handle 'room_started' event."""
@@ -112,7 +112,7 @@ class ParticipantsManagement:
await lkapi.aclose()
@async_to_sync
async def update( # noqa: PLR0917
async def update(
self,
room_name: str,
identity: str,
@@ -8,7 +8,6 @@ from typing import Dict, Optional
from asgiref.sync import async_to_sync
from livekit.api import (
DeleteRoomRequest,
ListRoomsRequest,
TwirpError,
UpdateRoomMetadataRequest,
@@ -89,30 +88,3 @@ class RoomManagement:
finally:
await lkapi.aclose()
@async_to_sync
async def delete_room(self, room_name: str):
"""Delete a LiveKit room and disconnect all participants.
Raises:
RoomNotFoundException: the room does not exist in LiveKit.
RoomManagementException: the deletion otherwise fails.
"""
lkapi = utils.create_livekit_client()
try:
await lkapi.room.delete_room(DeleteRoomRequest(room=room_name))
logger.info("Deleted LiveKit room %s", room_name)
except TwirpError as e:
if e.code == "not_found":
logger.warning(
"Room %s not found in LiveKit, skipping deletion",
room_name,
)
raise RoomNotFoundException("Room does not exist") from e
logger.exception("Unexpected error deleting room %s", room_name)
raise RoomManagementException("Could not delete room") from e
finally:
await lkapi.aclose()
-9
View File
@@ -1,9 +0,0 @@
"""Celery tasks for the core app."""
from core.tasks.connection_test import delete_connection_test_room
from core.tasks.file import process_file_deletion
__all__ = (
"delete_connection_test_room",
"process_file_deletion",
)
-39
View File
@@ -1,39 +0,0 @@
"""Tasks related to connection test rooms."""
import logging
from django.conf import settings
from core.services.room_management import (
RoomManagement,
RoomManagementException,
RoomNotFoundException,
)
from core.tasks._task import task
logger = logging.getLogger(__name__)
@task
def delete_connection_test_room(room_name: str):
"""Force-delete an ephemeral connection-test room.
Used as a hard cap so a participant cannot keep an auto-refreshed
LiveKit session open indefinitely after requesting a test token.
"""
prefix = settings.CONNECTION_TEST_ROOM_PREFIX
if not room_name.startswith(prefix):
logger.error(
"Refusing to delete room '%s': expected prefix '%s'.",
room_name,
prefix,
)
return
try:
RoomManagement().delete_room(room_name)
except RoomNotFoundException:
# Room may already be gone after empty/departure timeout.
logger.info("Connection test room '%s' already gone.", room_name)
except RoomManagementException:
logger.exception("Failed to delete connection test room '%s'.", room_name)
@@ -5,7 +5,6 @@ Test event notification.
# pylint: disable=assignment-from-no-return,redefined-outer-name,unused-argument,protected-access
import datetime
import json
import smtplib
from unittest import mock
@@ -419,63 +418,3 @@ def test_notify_summary_service_post_args_without_metadata(
mock_is_feature_flag_enabled.assert_called_once_with(
owner, UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
)
@mock.patch("core.recording.event.notification.requests.post")
@mock.patch("core.recording.event.notification.generate_download_s3_url")
@mock.patch.object(
NotificationService, "_get_recording_timestamps", new_callable=mock.AsyncMock
)
def test_notify_summary_service_v2_payload_json_serializable_without_timestamps(
mock_get_recording_timestamps,
mock_generate_download_s3_url,
mock_post,
settings,
):
"""Regression test for a non-JSON-serializable payload when timestamps are missing.
When the LiveKit egress can no longer be found, ``_get_recording_timestamps``
returns ``(None, None)`` and ``_generate_title`` falls back to its default
title. That default must be a real ``str``: it used to return a lazy
``gettext_lazy`` proxy, which ``json.dumps`` cannot serialize, so the real
``requests.post(json=payload)`` call crashed in production with
``TypeError: Object of type __proxy__ is not JSON serializable``.
"""
settings.SUMMARY_SERVICE_VERSION = 2
settings.SUMMARY_SERVICE_ENDPOINT = "https://summary.test/api/v2/tasks"
settings.SUMMARY_SERVICE_API_TOKEN = "summary-token"
settings.RECORDING_DOWNLOAD_BASE_URL = "https://app.test/recordings"
settings.SCREEN_RECORDING_BASE_URL = None
settings.METADATA_COLLECTOR_ENABLED = False
recording = factories.RecordingFactory(room__name="Daily")
owner = factories.UserFactory(
email="owner@test.com",
sub="owner-sub",
language="fr-fr",
timezone="Europe/Paris",
)
factories.UserRecordingAccessFactory(
recording=recording, role=models.RoleChoices.OWNER, user=owner
)
# Egress timestamps unavailable -> default-title branch in _generate_title.
mock_get_recording_timestamps.return_value = (None, None)
mock_generate_download_s3_url.return_value = "https://storage.test/recording.mp4"
mock_response = mock.Mock()
mock_response.raise_for_status.return_value = None
mock_response.json.return_value = {"job_id": "job-77"}
mock_post.return_value = mock_response
result = NotificationService._notify_summary_service(recording)
assert result is True
payload = mock_post.call_args.kwargs["json"]
title = payload["push_to_docs_config"]["title"]
# The title must be a plain ``str``, not a lazy translation proxy...
assert isinstance(title, str)
# ...so the payload serializes exactly the way ``requests`` serializes it.
json.dumps(payload)
@@ -75,7 +75,7 @@ def test_initialization(
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
def test_handle_egress_ended_success( # noqa: PLR0913, PLR0917 # pylint: disable=too-many-arguments, too-many-positional-arguments
def test_handle_egress_ended_success( # noqa: PLR0913 # pylint: disable=too-many-arguments, too-many-positional-arguments
mock_update_metadata, mock_notify, mode, notification_type, service, settings
):
"""Should successfully stop recording and notifies all participant."""
@@ -159,7 +159,7 @@ def test_handle_egress_updated_non_handled(
)
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.room_management.RoomManagement.update_metadata")
def test_handle_egress_ended_metadata_update_fails( # noqa: PLR0913, PLR0917 # pylint: disable=too-many-arguments, too-many-positional-arguments
def test_handle_egress_ended_metadata_update_fails( # noqa: PLR0913 # pylint: disable=too-many-arguments, too-many-positional-arguments
mock_update_metadata, mock_notify, mode, notification_type, service, settings
):
"""Should successfully stop and save recording when metadata's update fails."""
@@ -350,7 +350,7 @@ def test_handle_egress_ended_does_not_call_metadata_collector_stop_when_conditio
"notify_return_value, recording_status",
[(True, "notification_succeeded"), (False, "saved")],
)
def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913, PLR0917
def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913
mock_update_metadata,
mock_notify,
mock_notify_external_services,
@@ -392,7 +392,7 @@ def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913, PLR0917
(EgressStatus.EGRESS_LIMIT_REACHED, "stopped"),
],
)
def test_handle_egress_ended_does_not_finalize_when_webhooks_enabled( # noqa: PLR0913, PLR0917
def test_handle_egress_ended_does_not_finalize_when_webhooks_enabled( # noqa: PLR0913
mock_update_metadata,
mock_notify,
mock_notify_external_services,
@@ -720,7 +720,6 @@ def test_receive_unsupported_event(mock_receive, service):
# Mock returned data with unsupported event type
mock_data = mock.MagicMock()
mock_data.room.name = str(uuid.uuid4())
mock_data.event = "unsupported_event"
mock_receive.return_value = mock_data
@@ -824,33 +823,3 @@ def test_receive_filter_processes_matching_events(
service.receive(mock_request)
mock_handle_room_started.assert_called_once()
@mock.patch.object(api.WebhookReceiver, "receive")
@mock.patch.object(LiveKitEventsService, "_handle_room_finished")
@mock.patch.object(LiveKitEventsService, "_handle_room_started")
def test_receive_ignores_connection_test_room(
mock_handle_room_started,
mock_handle_room_finished,
mock_receive,
mock_livekit_config,
settings,
):
"""Should ignore all webhook events for connection test rooms in receive()."""
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
mock_request = mock.MagicMock()
mock_request.headers = {"Authorization": "test_token"}
mock_request.body = b"{}"
mock_data = mock.MagicMock()
mock_data.room.name = f"{settings.CONNECTION_TEST_ROOM_PREFIX}-{uuid.uuid4()}"
mock_data.event = "room_started"
mock_receive.return_value = mock_data
service = LiveKitEventsService()
service.receive(mock_request)
mock_handle_room_started.assert_not_called()
mock_handle_room_finished.assert_not_called()
@@ -3,7 +3,7 @@ Test lobby service.
"""
# pylint: disable=W0621,W0613, W0212, R0913
# ruff: noqa: PLR0913, PLR0917
# ruff: noqa: PLR0913
import uuid
from unittest import mock
@@ -1,60 +0,0 @@
"""Tests for the RoomManagement service."""
from unittest import mock
import pytest
from livekit.api import TwirpError
from core.services.room_management import (
RoomManagement,
RoomManagementException,
RoomNotFoundException,
)
@mock.patch("core.services.room_management.utils.create_livekit_client")
def test_delete_room_calls_livekit(mock_create_livekit_client):
"""DeleteRoom is forwarded to the LiveKit API."""
mock_api = mock.MagicMock()
mock_api.room.delete_room = mock.AsyncMock()
mock_api.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api
RoomManagement().delete_room("room-abc")
mock_api.room.delete_room.assert_awaited_once()
request = mock_api.room.delete_room.await_args.args[0]
assert request.room == "room-abc"
mock_api.aclose.assert_awaited_once()
@mock.patch("core.services.room_management.utils.create_livekit_client")
def test_delete_room_raises_not_found(mock_create_livekit_client):
"""Missing rooms raise RoomNotFoundException."""
mock_api = mock.MagicMock()
mock_api.room.delete_room = mock.AsyncMock(
side_effect=TwirpError("not_found", "room not found", status=404)
)
mock_api.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api
with pytest.raises(RoomNotFoundException):
RoomManagement().delete_room("missing-room")
mock_api.aclose.assert_awaited_once()
@mock.patch("core.services.room_management.utils.create_livekit_client")
def test_delete_room_raises_management_exception(mock_create_livekit_client):
"""Unexpected Twirp errors raise RoomManagementException."""
mock_api = mock.MagicMock()
mock_api.room.delete_room = mock.AsyncMock(
side_effect=TwirpError("internal", "boom", status=500)
)
mock_api.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api
with pytest.raises(RoomManagementException):
RoomManagement().delete_room("room-abc")
mock_api.aclose.assert_awaited_once()
@@ -1,51 +0,0 @@
"""Tests for connection test Celery tasks."""
from unittest import mock
from django.test.utils import override_settings
from core.services.room_management import (
RoomManagementException,
RoomNotFoundException,
)
from core.tasks.connection_test import delete_connection_test_room
@mock.patch("core.tasks.connection_test.RoomManagement.delete_room")
def test_delete_connection_test_room_calls_room_management(mock_delete_room, settings):
"""RoomManagement.delete_room is called for connection-test rooms."""
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
delete_connection_test_room("connection-test-abc")
mock_delete_room.assert_called_once_with("connection-test-abc")
@mock.patch("core.tasks.connection_test.RoomManagement.delete_room")
def test_delete_connection_test_room_refuses_other_rooms(mock_delete_room, settings):
"""Refuse to delete rooms outside the connection-test namespace."""
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
delete_connection_test_room("production-room")
mock_delete_room.assert_not_called()
@mock.patch("core.tasks.connection_test.RoomManagement.delete_room")
def test_delete_connection_test_room_ignores_missing_room(mock_delete_room, settings):
"""Missing rooms are treated as already cleaned up."""
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
mock_delete_room.side_effect = RoomNotFoundException("Room does not exist")
delete_connection_test_room("connection-test-gone")
mock_delete_room.assert_called_once_with("connection-test-gone")
@mock.patch("core.tasks.connection_test.RoomManagement.delete_room")
def test_delete_connection_test_room_logs_other_failures(mock_delete_room, settings):
"""Unexpected LiveKit failures are swallowed after logging."""
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
mock_delete_room.side_effect = RoomManagementException("Could not delete room")
delete_connection_test_room("connection-test-fail")
mock_delete_room.assert_called_once_with("connection-test-fail")
@@ -1,166 +0,0 @@
"""Test diagnostics API endpoints."""
import uuid
from unittest import mock
from django.test.utils import override_settings
from django.urls import reverse
import jwt
import pytest
from rest_framework.test import APIClient
from core.api.throttling import (
ConnectionTestAnonRateThrottle,
ConnectionTestUserRateThrottle,
)
from core.factories import UserFactory
pytestmark = pytest.mark.django_db
def test_api_diagnostics_connection_url():
"""The connection check is exposed under the diagnostics namespace."""
assert reverse("diagnostics-connection") == "/api/v1.0/diagnostics/connection/"
def test_api_diagnostics_connection_rejects_get():
"""Only POST is exposed, the endpoint has no side effect to trigger."""
client = APIClient()
response = client.get("/api/v1.0/diagnostics/connection/")
assert response.status_code == 405
def test_api_diagnostics_connection_returns_ephemeral_livekit_config(settings, client):
"""Each request gets a dedicated room and a short-lived token."""
settings.CONNECTION_TEST_TOKEN_TTL_SECONDS = 600
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
response_a = client.post("/api/v1.0/diagnostics/connection/")
response_b = client.post("/api/v1.0/diagnostics/connection/")
assert response_a.status_code == 200
assert response_b.status_code == 200
data_a = response_a.json()
data_b = response_b.json()
room_a = data_a["livekit"]["room"]
room_b = data_b["livekit"]["room"]
assert room_a.startswith("connection-test-")
assert room_b.startswith("connection-test-")
uuid.UUID(room_a.removeprefix("connection-test-"))
uuid.UUID(room_b.removeprefix("connection-test-"))
assert room_a != room_b
assert data_a["livekit"]["url"]
assert data_a["livekit"]["token"]
assert data_a["livekit"]["expires_in"] == 600
assert data_a["livekit"]["token"] != data_b["livekit"]["token"]
def test_api_diagnostics_connection_token_is_short_lived_for_user(settings, client):
"""Connection test tokens expire quickly for users."""
settings.CONNECTION_TEST_TOKEN_TTL_SECONDS = 300
client = APIClient()
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 200
config = response.json()["livekit"]
payload = jwt.decode(
config["token"],
settings.LIVEKIT_CONFIGURATION["api_secret"],
algorithms=["HS256"],
options={"verify_exp": False},
)
assert config["expires_in"] == 300
assert payload["video"]["room"] == config["room"]
assert payload["name"] == "Connection Test"
assert payload["video"]["roomAdmin"] is False
assert payload["exp"] - payload["nbf"] == 300
@override_settings()
def test_api_diagnostics_connection_token_for_authenticated_user(settings, client):
"""Logged-in users get a token bound to their own identity."""
settings.CONNECTION_TEST_TOKEN_TTL_SECONDS = 300
user = UserFactory()
client.force_login(user)
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 200
payload = jwt.decode(
response.json()["livekit"]["token"],
settings.LIVEKIT_CONFIGURATION["api_secret"],
algorithms=["HS256"],
options={"verify_exp": False},
)
assert payload["sub"] == str(user.sub)
assert payload["video"]["roomAdmin"] is False
assert payload["exp"] - payload["nbf"] == 300
@mock.patch("core.api.viewsets.delete_connection_test_room.apply_async")
def test_api_diagnostics_connection_schedules_room_deletion(
mock_apply_async, settings, client
):
"""When Celery is enabled, schedule a hard room delete after max age."""
settings.CELERY_ENABLED = True
settings.CONNECTION_TEST_TOKEN_TTL_SECONDS = 300
settings.CONNECTION_TEST_ROOM_EXTRA_AGE_SECONDS = 10
settings.CONNECTION_TEST_ROOM_PREFIX = "connection-test"
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 200
room = response.json()["livekit"]["room"]
mock_apply_async.assert_called_once_with(args=[room], countdown=310)
@mock.patch("core.api.viewsets.delete_connection_test_room.apply_async")
def test_api_diagnostics_connection_skips_room_deletion_without_celery(
mock_apply_async, settings, client
):
"""Without Celery, do not schedule deletion (apply_async would run immediately)."""
settings.CELERY_ENABLED = False
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 200
mock_apply_async.assert_not_called()
@pytest.mark.parametrize(
"throttle_class",
[ConnectionTestAnonRateThrottle, ConnectionTestUserRateThrottle],
)
def test_api_diagnostics_connection_is_throttled(throttle_class, client):
"""Both throttles stay wired to the action once routed through the viewset."""
with (
mock.patch.object(throttle_class, "allow_request", return_value=False),
mock.patch.object(throttle_class, "wait", return_value=42),
):
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 429
def test_api_diagnostics_connection_feature_flag(client, settings):
"""Should return a not found error when the connection diagnostics feature is disabled."""
settings.CONNECTION_TEST_ENABLED = False
response = client.post("/api/v1.0/diagnostics/connection/")
assert response.status_code == 404
-5
View File
@@ -30,11 +30,6 @@ router.register(
addons_viewsets.SessionViewSet,
basename="addons_sessions",
)
router.register(
"diagnostics",
viewsets.DiagnosticsViewSet,
basename="diagnostics",
)
# - External API
external_router = SimpleRouter()
+2 -7
View File
@@ -12,7 +12,6 @@ import mimetypes
import random
import secrets
import string
from datetime import timedelta
from functools import lru_cache
from typing import List, Optional
from uuid import uuid4
@@ -60,7 +59,7 @@ def generate_color(identity: str) -> str:
return f"hsl({hue}, {saturation}%, {lightness}%)"
def generate_token( # noqa: PLR0917
def generate_token(
room: str,
user,
username: Optional[str] = None,
@@ -68,7 +67,6 @@ def generate_token( # noqa: PLR0917
sources: Optional[List[str]] = None,
role: Optional[str] = None,
participant_id: Optional[str] = None,
ttl: Optional[timedelta] = None,
) -> str:
"""Generate a LiveKit access token for a user in a specific room.
@@ -84,7 +82,6 @@ def generate_token( # noqa: PLR0917
role (Optional[str]): Room's access role if any
participant_id (Optional[str]): Stable identifier for anonymous users;
used as identity when user.is_anonymous.
ttl (Optional[timedelta]): Token validity duration. Defaults to LiveKit SDK default.
Returns:
str: The LiveKit JWT access token.
@@ -138,13 +135,11 @@ def generate_token( # noqa: PLR0917
}
)
)
if ttl is not None:
token = token.with_ttl(ttl)
return token.to_jwt()
def generate_livekit_config( # noqa: PLR0917
def generate_livekit_config(
room_id: str,
user,
username: str,
-31
View File
@@ -354,11 +354,6 @@ class Base(Configuration):
environ_name="ROOMKIT_JOIN_THROTTLE_RATES",
environ_prefix=None,
),
"connection_test": values.Value(
default="30/minute",
environ_name="CONNECTION_TEST_THROTTLE_RATES",
environ_prefix=None,
),
},
}
MONITORED_THROTTLE_FAILURE_CALLBACK = (
@@ -665,30 +660,6 @@ class Base(Configuration):
environ_prefix=None,
default=False,
)
CONNECTION_TEST_ENABLED = values.BooleanValue(
environ_name="CONNECTION_TEST_ENABLED",
environ_prefix=None,
default=False,
)
CONNECTION_TEST_TOKEN_TTL_SECONDS = values.PositiveIntegerValue(
300,
environ_name="CONNECTION_TEST_TOKEN_TTL_SECONDS",
environ_prefix=None,
)
# The effective room max age is always computed as
# CONNECTION_TEST_TOKEN_TTL_SECONDS + this value. Token expiration does
# not automatically delete rooms, so once that age is reached, the
# cleanup worker will explicitly delete the room if it still exists.
CONNECTION_TEST_ROOM_EXTRA_AGE_SECONDS = values.PositiveIntegerValue(
10,
environ_name="CONNECTION_TEST_ROOM_EXTRA_AGE_SECONDS",
environ_prefix=None,
)
CONNECTION_TEST_ROOM_PREFIX = values.Value(
"connection-test",
environ_name="CONNECTION_TEST_ROOM_PREFIX",
environ_prefix=None,
)
LIVEKIT_VERIFY_SSL = values.BooleanValue(
True, environ_name="LIVEKIT_VERIFY_SSL", environ_prefix=None
)
@@ -1299,8 +1270,6 @@ class Test(Base):
ADDONS_CSRF_SECRET = "secret-key-padded-for-minimum-len!-addons" # noqa:S105
ADDONS_TOKEN_SECRET_KEY = "secret-key-padded-for-minimum-len!-addons" # noqa:S105
CONNECTION_TEST_ENABLED = True
def __init__(self):
# pylint: disable=invalid-name
self.INSTALLED_APPS += ["drf_spectacular_sidecar"]
+14 -15
View File
@@ -24,7 +24,7 @@ keywords = ["Django", "Contacts", "Templates", "RBAC"]
license = "MIT"
requires-python = ">=3.13"
dependencies = [
"boto3==1.43.56",
"boto3==1.43.36",
"Brotli==1.2.0",
"brevo-python==1.2.0",
"celery[redis]==5.6.3",
@@ -32,7 +32,7 @@ dependencies = [
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-countries==9.0.0",
"django-filter==26.1",
"django-filter==25.2",
"django-lasuite[all]==0.0.27",
"django-parler==2.4",
"redis==5.2.1",
@@ -42,7 +42,7 @@ dependencies = [
"django-pydantic-field==0.5.4",
"django==5.2.16",
"djangorestframework==3.17.1",
"drf_spectacular==0.30.0",
"drf_spectacular==0.29.0",
"dockerflow==2026.3.4",
"easy_thumbnails==2.10.1",
"factory_boy==3.3.3",
@@ -50,21 +50,20 @@ dependencies = [
"jsonschema==4.26.0",
"markdown==3.10.2",
"nested-multipart-parser==1.6.0",
"posthog==7.29.0",
"posthog==7.16.1",
"psycopg[binary]==3.3.4",
"pydantic==2.13.4",
"PyJWT==2.13.0",
"python-frontmatter==1.3.0",
"python-magic==0.4.27",
"requests==2.34.2",
"sentry-sdk==2.66.1",
"sentry-sdk==2.63.0",
"whitenoise==6.12.0",
"mozilla-django-oidc==5.0.2",
"livekit-api==1.2.0",
"aiohttp==3.14.3",
"livekit-api==1.1.1",
"aiohttp==3.14.1",
"urllib3==2.7.0",
"phonenumbers==9.0.34",
"cryptography==50.0.0", # CVE-2026-69247
"phonenumbers==9.0.33",
]
[project.urls]
@@ -76,21 +75,21 @@ dependencies = [
[dependency-groups]
dev = [
"django-extensions==4.1",
"drf-spectacular-sidecar==2026.7.1",
"drf-spectacular-sidecar==2026.6.1",
"freezegun==1.5.5",
"ipdb==0.13.13",
"ipython==9.15.0",
"ipython==9.14.1",
"pyfakefs==6.2.0",
"pylint-django==2.8.0",
"pylint-django==2.7.0",
"pylint<4.0.0",
"pytest-cov==7.1.0",
"pytest-django==4.12.0",
"pytest==9.1.1",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.26.2",
"ruff==0.16.0",
"types-requests==2.33.0.20260712",
"responses==0.26.1",
"ruff==0.15.19",
"types-requests==2.33.0.20260518",
]
[tool.uv.build-backend]
+182 -185
View File
@@ -13,7 +13,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.14.3"
version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -24,72 +24,72 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
{ url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
{ url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
{ url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
{ url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
{ url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
{ url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
{ url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
{ url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
{ url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
{ url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
{ url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
{ url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
{ url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
{ url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
{ url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
{ url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
{ url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
{ url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
{ url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
{ url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
{ url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
{ url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
{ url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
{ url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
{ url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
{ url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
{ url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
{ url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
{ url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
{ url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
{ url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
{ url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
{ url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
{ url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
{ url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
{ url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
{ url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
{ url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
{ url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
{ url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
{ url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
{ url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
{ url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
{ url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
{ url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
{ url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
{ url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
{ url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
]
[[package]]
@@ -181,30 +181,30 @@ wheels = [
[[package]]
name = "boto3"
version = "1.43.56"
version = "1.43.36"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/05/23e1aa8c9e4b0399a61e7fd65c4f9cc0625121f24760e37471f776404abb/boto3-1.43.56.tar.gz", hash = "sha256:57c90df9fb026f2e6ae22530861198130203733c5c9ec4e5cca3a4037f5a8db4", size = 112673, upload-time = "2026-07-24T19:31:48.606Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/897287e955db0f50b12fd69ef45956e4fd2c7ddb48c736872f7ea2314443/boto3-1.43.36.tar.gz", hash = "sha256:587d7ee92a12e440ad12b0e7f11f3358f0c4d65b19f64726efc94aaf194aff28", size = 112690, upload-time = "2026-06-23T02:47:14.561Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/57/3a960c9f581c00f2a591901b46e035ff79ab3956d16607f12306b3b8d483/boto3-1.43.56-py3-none-any.whl", hash = "sha256:feb699d4ab241ef5c1b80bb58277be2aaad365cd4b672d7817e0bc59ee45131b", size = 140026, upload-time = "2026-07-24T19:31:47.155Z" },
{ url = "https://files.pythonhosted.org/packages/9f/f1/274303f52483ecf199eae6f8d9b6f5951670397ee4d72c06cfd4eb644612/boto3-1.43.36-py3-none-any.whl", hash = "sha256:42942dde254673abcbc9e6e60017c88341a4f49d99d24e1f2e290fb38138c26f", size = 140031, upload-time = "2026-06-23T02:47:13.178Z" },
]
[[package]]
name = "botocore"
version = "1.43.62"
version = "1.43.40"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/36af6d99269a701f83809b87a01f4728699eb825ebdedee3a3d515b18f61/botocore-1.43.62.tar.gz", hash = "sha256:94efc419c9f0f41dc2415e4b6b62f04ae21b3ce3930fac47214c4d3f361ea8b8", size = 15818261, upload-time = "2026-07-31T19:35:06.235Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/50/269986277f852cc83029bccbdcdc0b343a685cfd570599e58029792808d8/botocore-1.43.40.tar.gz", hash = "sha256:2085a4314cfd2c8bc1d08ab8039f76c92e99278db0d2a0e2437010526d5d5d70", size = 15639899, upload-time = "2026-07-03T00:28:16.125Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/65/d5dae96de68ffc55acf87c3bae76e9dabeeca92aadf1223f20e9a7860aef/botocore-1.43.62-py3-none-any.whl", hash = "sha256:76de153de1ba3e242b2e6df6a13ab8a3fb35d17db562462969e661457b63166e", size = 15502622, upload-time = "2026-07-31T19:35:02.697Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8c/5f7e73fd66b28f0705bc55d7060d41ef72328b656b86ca53e75765b3ba2c/botocore-1.43.40-py3-none-any.whl", hash = "sha256:0bc9d352267c9e48415c5d7bb61ff05c3f193eac2fc7e69cfd229a05fbab67d6", size = 15323870, upload-time = "2026-07-03T00:28:12.56Z" },
]
[[package]]
@@ -497,52 +497,52 @@ wheels = [
[[package]]
name = "cryptography"
version = "50.0.0"
version = "49.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
]
[[package]]
@@ -650,14 +650,14 @@ wheels = [
[[package]]
name = "django-filter"
version = "26.1"
version = "25.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/563965173d4cbb5fc308087e7b3d11a115b7b67273d093622480b1e31f78/django_filter-26.1.tar.gz", hash = "sha256:66ea04031b068c77c86e1ac26ced7a3f8f13ce797f5795751707e3deefc58054", size = 144299, upload-time = "2026-07-11T09:27:02.767Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" },
{ url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" },
]
[[package]]
@@ -775,7 +775,7 @@ wheels = [
[[package]]
name = "drf-spectacular"
version = "0.30.0"
version = "0.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
@@ -785,21 +785,21 @@ dependencies = [
{ name = "pyyaml" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/43/41d25039a6a53545420ebc98eb9f877ec9fe30c7bd03fefabcaf9b953af7/drf_spectacular-0.30.0.tar.gz", hash = "sha256:53e79e7ba00e240441b63c32273754a5368e4c2ab44a19f2595277cc1cd559c9", size = 252311, upload-time = "2026-07-06T11:29:46.264Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/56/74dd7b45bbde6d24494220b98d6961cb1200b63a1800332b430daa2c4551/drf_spectacular-0.30.0-py3-none-any.whl", hash = "sha256:006cf5921ebe20a9bd24f7c846261ebbf78780be5961b0d6e87afaa82afd62ff", size = 111150, upload-time = "2026-07-06T11:29:45.12Z" },
{ url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" },
]
[[package]]
name = "drf-spectacular-sidecar"
version = "2026.7.1"
version = "2026.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/51/9e038d14bf51a0bd051e8bcb690287349c908fa7ba69021d9f3e5d5ac51f/drf_spectacular_sidecar-2026.7.1.tar.gz", hash = "sha256:40113c4066c7bc3ef15a7ce1c40cda227a907a9986748024a813a9e0595eba25", size = 2593211, upload-time = "2026-07-01T13:39:06.084Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7f/d8/735b129e7d55c4f6c682ecedfcc0438816e1d859e5e1837f914ac4544f6d/drf_spectacular_sidecar-2026.6.1.tar.gz", hash = "sha256:e159874fa85ccee39b801e260f2a3585fbe36a0c79bf811824eef9010ab98ea9", size = 2589761, upload-time = "2026-06-01T16:45:30.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/d08f5c79f7643dbff4512605c28b75481966ed6c8cc9b397c9dd2ee91cd1/drf_spectacular_sidecar-2026.7.1-py3-none-any.whl", hash = "sha256:bc6d50c9b64660e45e09296d39553b3e759eedd825fc41d631ee5b3f88e0c5de", size = 2617384, upload-time = "2026-07-01T13:39:03.787Z" },
{ url = "https://files.pythonhosted.org/packages/4f/46/10bcaf965edcb70e7647e62b88b9e0de266f8bea7a7a3224e8f977f77eab/drf_spectacular_sidecar-2026.6.1-py3-none-any.whl", hash = "sha256:4560572773c7e5f636d36cd2903204e2c59560af0548da254e311f94458c51a2", size = 2613235, upload-time = "2026-06-01T16:45:29.031Z" },
]
[[package]]
@@ -1005,7 +1005,7 @@ wheels = [
[[package]]
name = "ipython"
version = "9.15.0"
version = "9.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1015,14 +1015,14 @@ dependencies = [
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
{ name = "psutil", marker = "sys_platform != 'emscripten'" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" },
{ url = "https://files.pythonhosted.org/packages/9d/22/58818a63eaf8982b67632b1bc20585c811611b15a8da19d6012323dc76a5/ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c", size = 627770, upload-time = "2026-06-05T08:12:33.045Z" },
]
[[package]]
@@ -1128,7 +1128,7 @@ redis = [
[[package]]
name = "livekit-api"
version = "1.2.0"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -1137,22 +1137,22 @@ dependencies = [
{ name = "pyjwt" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/19/36ff6712ec638a4b7dad4d8f03795952e401dc31db0b04cddec7892650da/livekit_api-1.2.0.tar.gz", hash = "sha256:a89817b3bca9584873786ff07209839308217537a42f95ecb2609aafaa109ddc", size = 20778, upload-time = "2026-07-11T23:20:54.781Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/03/00e0ec173f247e1f7ea63cb5591d5680a64c7a74ea4d5d558e5aed6cc399/livekit_api-1.1.1.tar.gz", hash = "sha256:70c7b80eecbc297b40756ebd76e4f52d00b0348fb7d212a21c1f69cc57fd9c83", size = 15196, upload-time = "2026-06-24T01:36:19.686Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/e7/8926f16d4bc1b2e0ae46d4a507321bb899396d263a757f1adaabcd3b3867/livekit_api-1.2.0-py3-none-any.whl", hash = "sha256:307f8e5cfb0358c3ca091814ab768af55896022151bcd7f951954ccefa036a24", size = 26499, upload-time = "2026-07-11T23:20:53.736Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c0/d5f3ff74ab5db2d06f173801ec934885d11a754b9fb9ad768c8ede0a6c89/livekit_api-1.1.1-py3-none-any.whl", hash = "sha256:ce8c327676c366e66cf68782934368dd0ba92b9d48f578275227e255c890fe88", size = 19471, upload-time = "2026-06-24T01:36:18.42Z" },
]
[[package]]
name = "livekit-protocol"
version = "1.1.21"
version = "1.1.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
{ name = "types-protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/ae/9d60fe37d85623e68a2e36ae31d18c671949db67d2a13a6438410d930466/livekit_protocol-1.1.21.tar.gz", hash = "sha256:8bb1ac1aba5d37d0af43e9d56d129a5d16295cbd91518b00fd157e258f20a6ef", size = 122363, upload-time = "2026-07-21T18:28:26.372Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/88/64f2be01a630e249f1dbd0d51876f109b53b7899ae41246d2ca5b647086d/livekit_protocol-1.1.18.tar.gz", hash = "sha256:187af32ebf75333a62117b0db9e551c99060bd4e1f57cfc0fce73bcd7a671da8", size = 115802, upload-time = "2026-06-27T15:31:04.102Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/d2/ec10b1cdf912235c2b07898be6ba2535e50f23e8980bb719f29decbd8034/livekit_protocol-1.1.21-py3-none-any.whl", hash = "sha256:ce0bb763327c91349ee8831843c4f8bd72132c4d06ac572171f2ae5c0217211d", size = 149245, upload-time = "2026-07-21T18:28:24.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/80/9cc33e4d0280132538850aaf9559d6b8aa9e670c5917f75dab996400ab84/livekit_protocol-1.1.18-py3-none-any.whl", hash = "sha256:30c539410fd3cfc2e551ca3a193aaaaacaaec6dd57dabe2c9be7c7c7d15f0e01", size = 143134, upload-time = "2026-06-27T15:31:02.686Z" },
]
[[package]]
@@ -1195,7 +1195,6 @@ dependencies = [
{ name = "brevo-python" },
{ name = "brotli" },
{ name = "celery", extra = ["redis"] },
{ name = "cryptography" },
{ name = "dj-database-url" },
{ name = "django" },
{ name = "django-configurations" },
@@ -1255,18 +1254,17 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = "==3.14.3" },
{ name = "boto3", specifier = "==1.43.56" },
{ name = "aiohttp", specifier = "==3.14.1" },
{ name = "boto3", specifier = "==1.43.36" },
{ name = "brevo-python", specifier = "==1.2.0" },
{ name = "brotli", specifier = "==1.2.0" },
{ name = "celery", extras = ["redis"], specifier = "==5.6.3" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "dj-database-url", specifier = "==3.1.2" },
{ name = "django", specifier = "==5.2.16" },
{ name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==9.0.0" },
{ name = "django-filter", specifier = "==26.1" },
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.27" },
{ name = "django-parler", specifier = "==2.4" },
{ name = "django-pydantic-field", specifier = "==0.5.4" },
@@ -1275,17 +1273,17 @@ requires-dist = [
{ name = "django-timezone-field", specifier = ">=5.1" },
{ name = "djangorestframework", specifier = "==3.17.1" },
{ name = "dockerflow", specifier = "==2026.3.4" },
{ name = "drf-spectacular", specifier = "==0.30.0" },
{ name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "easy-thumbnails", specifier = "==2.10.1" },
{ name = "factory-boy", specifier = "==3.3.3" },
{ name = "gunicorn", specifier = "==26.0.0" },
{ name = "jsonschema", specifier = "==4.26.0" },
{ name = "livekit-api", specifier = "==1.2.0" },
{ name = "livekit-api", specifier = "==1.1.1" },
{ name = "markdown", specifier = "==3.10.2" },
{ name = "mozilla-django-oidc", specifier = "==5.0.2" },
{ name = "nested-multipart-parser", specifier = "==1.6.0" },
{ name = "phonenumbers", specifier = "==9.0.34" },
{ name = "posthog", specifier = "==7.29.0" },
{ name = "phonenumbers", specifier = "==9.0.33" },
{ name = "posthog", specifier = "==7.16.1" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.4" },
{ name = "pydantic", specifier = "==2.13.4" },
{ name = "pyjwt", specifier = "==2.13.0" },
@@ -1293,7 +1291,7 @@ requires-dist = [
{ name = "python-magic", specifier = "==0.4.27" },
{ name = "redis", specifier = "==5.2.1" },
{ name = "requests", specifier = "==2.34.2" },
{ name = "sentry-sdk", specifier = "==2.66.1" },
{ name = "sentry-sdk", specifier = "==2.63.0" },
{ name = "urllib3", specifier = "==2.7.0" },
{ name = "whitenoise", specifier = "==6.12.0" },
]
@@ -1301,21 +1299,21 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "django-extensions", specifier = "==4.1" },
{ name = "drf-spectacular-sidecar", specifier = "==2026.7.1" },
{ name = "drf-spectacular-sidecar", specifier = "==2026.6.1" },
{ name = "freezegun", specifier = "==1.5.5" },
{ name = "ipdb", specifier = "==0.13.13" },
{ name = "ipython", specifier = "==9.15.0" },
{ name = "ipython", specifier = "==9.14.1" },
{ name = "pyfakefs", specifier = "==6.2.0" },
{ name = "pylint", specifier = "<4.0.0" },
{ name = "pylint-django", specifier = "==2.8.0" },
{ name = "pylint-django", specifier = "==2.7.0" },
{ name = "pytest", specifier = "==9.1.1" },
{ name = "pytest-cov", specifier = "==7.1.0" },
{ name = "pytest-django", specifier = "==4.12.0" },
{ name = "pytest-icdiff", specifier = "==0.9" },
{ name = "pytest-xdist", specifier = "==3.8.0" },
{ name = "responses", specifier = "==0.26.2" },
{ name = "ruff", specifier = "==0.16.0" },
{ name = "types-requests", specifier = "==2.33.0.20260712" },
{ name = "responses", specifier = "==0.26.1" },
{ name = "ruff", specifier = "==0.15.19" },
{ name = "types-requests", specifier = "==2.33.0.20260518" },
]
[[package]]
@@ -1452,11 +1450,11 @@ wheels = [
[[package]]
name = "phonenumbers"
version = "9.0.34"
version = "9.0.33"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/86/c3/e154829a50679c38ae28ec9c4f151f2c425db5e70fd445266e76f6d6cd65/phonenumbers-9.0.34.tar.gz", hash = "sha256:00751c75d1166485ca80ce02ec15b6a61a2628e9b313381579330bc70c934075", size = 2306776, upload-time = "2026-07-03T06:30:37.358Z" }
sdist = { url = "https://files.pythonhosted.org/packages/75/37/dfc4cf24169f1a7169ebaedaf896c818f0add8603409d1e748e3085ccdc0/phonenumbers-9.0.33.tar.gz", hash = "sha256:9ab8a02b940b90c64f3866c0b25a30e567ddf7bb9836a3e11efdb0478f65fc1c", size = 2306756, upload-time = "2026-06-22T10:23:33.428Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/0a/3a7980f3b071dde9a297d85cf6b18ba4bc2e5024e1c453b3da8a1dc14268/phonenumbers-9.0.34-py2.py3-none-any.whl", hash = "sha256:1221bf8e65bd2c02770226488af806d4636814bc997104d3a1f7de6ed6410bd2", size = 2595344, upload-time = "2026-07-03T06:30:33.913Z" },
{ url = "https://files.pythonhosted.org/packages/e6/29/f7e30e3dbd3c7e3d9c4a55006112c04ee62b4765a31f21bcc28c253ac3f1/phonenumbers-9.0.33-py2.py3-none-any.whl", hash = "sha256:ba1d0da52711d5fdda6b2b673b2621fe80774fc5d1b2e5a6ef783396b0343186", size = 2595422, upload-time = "2026-06-22T10:23:29.925Z" },
]
[[package]]
@@ -1541,7 +1539,7 @@ wheels = [
[[package]]
name = "posthog"
version = "7.29.0"
version = "7.16.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
@@ -1549,9 +1547,9 @@ dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/09/43ae0e27bafe031d307921cdeadcd3af8a7370c887177df7c43cfd903adf/posthog-7.29.0.tar.gz", hash = "sha256:673f201e2d204f0664bb0cec86f8adfca97ba3a94488fb7857bb11c4a35fb017", size = 360483, upload-time = "2026-07-23T15:26:28.225Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b4/4f/a954175c862a3565d02c3f627874d85f18313472a0c4b08f45d84aaf3315/posthog-7.16.1.tar.gz", hash = "sha256:3619d3c619ad01f36c6d465e084950882417c63021eb3cfacacb23f900ec52d4", size = 226343, upload-time = "2026-05-27T18:46:20.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/4a/01c3e9f44a2f167977b5fafc9ba5b4ed5a027881da4b2ee1f4987a621758/posthog-7.29.0-py3-none-any.whl", hash = "sha256:8269afec8439e0177fd9d660b88d0a580497b617ee6aa213850ae9244ad46111", size = 429592, upload-time = "2026-07-23T15:26:26.343Z" },
{ url = "https://files.pythonhosted.org/packages/3e/28/0f840699a1d0db3c1e5483c6208f0804a51f21ccfa34e6aa356161606adc/posthog-7.16.1-py3-none-any.whl", hash = "sha256:fd5aa4510033f3b039fda2fbfce45f493d140d4782f681e69639793dda317d67", size = 264231, upload-time = "2026-05-27T18:46:17.933Z" },
]
[[package]]
@@ -1886,15 +1884,14 @@ wheels = [
[[package]]
name = "pylint-django"
version = "2.8.0"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pylint" },
{ name = "pylint-plugin-utils" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/a1/b92e5d5cf320b603c9bcc5174da7e9ba4c6ce71087354322a2b83536df13/pylint_django-2.8.0.tar.gz", hash = "sha256:42accea9098e4a3298b4bfbae0e4da81f909f8bff0deda9485efbd6035a86d6a", size = 32038, upload-time = "2026-07-11T10:19:14.844Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/4a/3dae8a09e12a28ccf3d7204cc4fb69f28dfb30d2b19567eb5ff094fe1265/pylint_django-2.8.0-py3-none-any.whl", hash = "sha256:706eb2cc8d7692236be9fd033a341042afe3bbbf99df9234a659db931016ef5d", size = 44672, upload-time = "2026-07-11T10:05:29.281Z" },
{ url = "https://files.pythonhosted.org/packages/e5/0d/d775fec0dde8ca5d20e9170a2ca332dfa21b77f7e7e47fc3ab9b2261773c/pylint_django-2.7.0-py3-none-any.whl", hash = "sha256:76ef7e7bbbcf7ee86adbb2beac0ffaa7232509a17bf4a488d81467a1bbaa215b", size = 42892, upload-time = "2026-01-01T11:17:04.292Z" },
]
[[package]]
@@ -2098,16 +2095,16 @@ wheels = [
[[package]]
name = "responses"
version = "0.26.2"
version = "0.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyyaml" },
{ name = "requests" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/1a/4af3e6d659394b809838490b144e4ab8d7ed3b9fecc7ca78f5d2f79b1a3d/responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8", size = 84030, upload-time = "2026-07-03T16:44:50.325Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/58/1fb6de3503428196df78638f991ec8095274f1ee9723e272ee4d9ff0092b/responses-0.26.1.tar.gz", hash = "sha256:2eb3218553cc8f79b57d257bac23af5e1bf381f5b9390b1767816f0843e01dc2", size = 83088, upload-time = "2026-05-21T19:56:39.747Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/28/693e1d9ebf72baa062ded80d837a035b86ce75eda5a269379e9e2b1008a8/responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364", size = 35609, upload-time = "2026-07-03T16:44:49.1Z" },
{ url = "https://files.pythonhosted.org/packages/3a/31/6a620b4427d546b9e7cca8b3b8c5f0559d9cef2bb9eedcda7f73c1473c19/responses-0.26.1-py3-none-any.whl", hash = "sha256:8aacc4586eb08fb2208ef64a9eb4258d9b0c6e6f4260845f2f018ab847495345", size = 35502, upload-time = "2026-05-21T19:56:38.046Z" },
]
[[package]]
@@ -2193,27 +2190,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.16.0"
version = "0.15.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
{ url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" },
{ url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" },
{ url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" },
{ url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" },
{ url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" },
{ url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" },
{ url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" },
{ url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" },
{ url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" },
{ url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" },
{ url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" },
{ url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" },
{ url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" },
{ url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" },
{ url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" },
{ url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" },
]
[[package]]
@@ -2230,15 +2227,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
version = "2.66.1"
version = "2.63.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/c8/b3c970a5b186722d276cd40a05b3254e03bccc0208560aff20f612e018e8/sentry_sdk-2.63.0.tar.gz", hash = "sha256:2a1502bf864769275dbc8c2c9fc7a0f7f5e18358180b615d262d13a31ffba216", size = 912449, upload-time = "2026-06-16T12:45:57.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" },
{ url = "https://files.pythonhosted.org/packages/7b/57/cb205f7d93373120f666b9c5736dc0815524d96a9b278e7a728f018dc22a/sentry_sdk-2.63.0-py3-none-any.whl", hash = "sha256:3a9b5ddd403f79eb73bd670f75f04485819db53d28f76ced7bc09041cb0dfd6a", size = 495950, upload-time = "2026-06-16T12:45:55.819Z" },
]
[[package]]
@@ -2302,14 +2299,14 @@ wheels = [
[[package]]
name = "types-requests"
version = "2.33.0.20260712"
version = "2.33.0.20260518"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" },
{ url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" },
]
[[package]]
-3
View File
@@ -36,9 +36,6 @@ WORKDIR /home/frontend
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
ARG VITE_APP_TITLE
ENV VITE_APP_TITLE=${VITE_APP_TITLE}
RUN npm run build
# ---- Front-end image ----
+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
-18
View File
@@ -1,18 +0,0 @@
{
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
-3
View File
@@ -45,9 +45,6 @@ export interface ApiConfig {
subtitle: {
enabled: boolean
}
diagnostics: {
connection_test_enabled?: boolean
}
telephony: {
enabled: boolean
international_phone_number?: string
+7 -59
View File
@@ -1,5 +1,5 @@
import { css, cva, RecipeVariantProps } from '@/styled-system/css'
import React, { useLayoutEffect, useMemo } from 'react'
import React from 'react'
const avatar = cva({
base: {
@@ -28,34 +28,13 @@ const avatar = cva({
},
})
// Instantiating a segmenter is expensive; create it once and reuse it.
const graphemeSegmenter =
typeof Intl !== 'undefined' && 'Segmenter' in Intl
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
: undefined
/**
* Returns the first user-perceived character. Some Unicode characters span
* multiple UTF-16 code units, so a naive index into the string can split them
* and yield a broken glyph.
*/
const getFirstGrapheme = (value: string): string => {
if (!value) return ''
if (graphemeSegmenter) {
const [first] = graphemeSegmenter.segment(value)
return first?.segment ?? ''
}
// Fallback: keeps single code points intact (including surrogate pairs).
return Array.from(value)[0] ?? ''
}
const getInitials = (name?: string): string => {
if (!name) return ''
const words = name.trim().split(/\s+/).filter(Boolean)
if (words.length === 0) return ''
const first = getFirstGrapheme(words[0])
const second = words.length > 1 ? getFirstGrapheme(words[1]) : ''
return (first + second).toLocaleUpperCase()
const first = words[0].charAt(0)
const second = words.length > 1 ? words[1].charAt(0) : ''
return (first + second).toUpperCase()
}
export type AvatarProps = React.HTMLAttributes<HTMLDivElement> & {
@@ -65,37 +44,7 @@ export type AvatarProps = React.HTMLAttributes<HTMLDivElement> & {
export const Avatar = React.memo(
({ name, bgColor, context, notification, style, ...props }: AvatarProps) => {
const initials = useMemo(() => getInitials(name), [name])
const textRef = React.useRef<SVGTextElement>(null)
const [offsetY, setOffsetY] = React.useState(0)
// Optically center the initials: measure the ink bounding box of the
// rendered glyphs and shift them so the box's center sits at the middle
// of the viewBox. Works for any font, weight or glyph shape, unlike a
// hand-tuned dy offset. getBBox() is in local (pre-transform)
// coordinates, so applying the translation never changes the measure.
useLayoutEffect(() => {
const text = textRef.current
if (!text) return
const center = () => {
const box = text.getBBox()
// A hidden element measures as an empty box; keep the default then.
if (box.height === 0) return
setOffsetY(50 - (box.y + box.height / 2))
}
center()
// Glyph metrics can change once webfonts finish loading.
let cancelled = false
document.fonts?.ready.then(() => {
if (!cancelled) center()
})
return () => {
cancelled = true
}
}, [initials])
const initials = getInitials(name)
return (
<div
style={{ backgroundColor: bgColor, ...style }}
@@ -108,17 +57,16 @@ export const Avatar = React.memo(
className={css({ width: '100%', height: '100%', display: 'block' })}
>
<text
ref={textRef}
x="50"
y="50"
transform={`translate(0 ${offsetY})`}
dy="-0.08em"
textAnchor="middle"
dominantBaseline="central"
fontSize="52"
fontWeight="500"
fill="currentColor"
>
{initials}
{initials.toUpperCase()}
</text>
</svg>
</div>
@@ -1,17 +0,0 @@
import { fetchApi } from '@/api/fetchApi'
export type LiveKitConnectionDetails = {
url: string
room: string
token: string
expires_in: number
}
export type ConnectionTestResponse = {
livekit: LiveKitConnectionDetails
}
export const fetchConnectionTestDetails = () =>
fetchApi<ConnectionTestResponse>('/diagnostics/connection/', {
method: 'POST',
})
@@ -1,257 +0,0 @@
import { Checker, Track, type CheckInfo } from 'livekit-client'
/**
* Addresses are useful to a network administrator (they identify the egress IP
* and the SFU endpoint actually reached) but they also land in a downloadable
* report. Flip this to false to keep only the candidate types and protocols.
*/
const INCLUDE_CANDIDATE_ADDRESSES = true
/** Beyond this, the log becomes noise rather than evidence. */
const MAX_LOGGED_PAIRS = 8
export type IceCandidateInfo = {
/** host, srflx, prflx or relay. */
type?: string
/** Transport to the first hop: udp or tcp. */
protocol?: string
/** Transport used by the relay itself (udp, tcp, tls). Local relay only. */
relayProtocol?: string
/** Chrome reports an mDNS `.local` name here for host candidates. */
address?: string
port?: number
/** Local candidates only, and not reported by every browser. */
networkType?: string
}
export type IceCandidatePair = {
/** The pair the browser is actually sending media on. */
selected: boolean
nominated?: boolean
local: IceCandidateInfo
remote: IceCandidateInfo
/** Round trip time in milliseconds. */
rttMs?: number
availableOutgoingBitrate?: number
bytesSent?: number
}
export type IceCandidateReport = {
selected: IceCandidatePair | null
/** Every pair that completed its connectivity checks, selected one first. */
working: IceCandidatePair[]
}
const PROBE_WIDTH = 320
const PROBE_HEIGHT = 180
const PROBE_FPS = 15
const SETTLE_DELAY_MS = 3000
type Stats = Record<string, unknown> & { type?: string }
const readCandidate = (stats?: Stats): IceCandidateInfo => {
if (!stats) return {}
return {
type: stats.candidateType as string | undefined,
protocol: stats.protocol as string | undefined,
relayProtocol: stats.relayProtocol as string | undefined,
networkType: stats.networkType as string | undefined,
...(INCLUDE_CANDIDATE_ADDRESSES
? {
address: stats.address as string | undefined,
port: stats.port as number | undefined,
}
: {}),
}
}
const describeCandidate = (candidate: IceCandidateInfo) => {
const transport = candidate.relayProtocol ?? candidate.protocol ?? 'unknown'
const endpoint =
candidate.address === undefined
? ''
: ` ${candidate.address}:${candidate.port ?? '?'}`
return `${candidate.type ?? 'unknown'} ${transport}${endpoint}`
}
const parseCandidates = (report: RTCStatsReport): IceCandidateReport => {
let selectedId: string | undefined
report.forEach((stats: Stats) => {
if (stats.type === 'transport' && stats.selectedCandidatePairId) {
selectedId = stats.selectedCandidatePairId as string
}
})
const working: IceCandidatePair[] = []
report.forEach((stats: Stats) => {
// `succeeded` means the pair completed its connectivity checks; failed,
// waiting and in-progress pairs are not evidence of anything working.
if (stats.type !== 'candidate-pair' || stats.state !== 'succeeded') return
const rtt = stats.currentRoundTripTime as number | undefined
working.push({
selected: selectedId !== undefined && stats.id === selectedId,
nominated: stats.nominated as boolean | undefined,
local: readCandidate(report.get(stats.localCandidateId as string)),
remote: readCandidate(report.get(stats.remoteCandidateId as string)),
rttMs: rtt === undefined ? undefined : Math.round(rtt * 1000),
availableOutgoingBitrate: stats.availableOutgoingBitrate as
| number
| undefined,
bytesSent: stats.bytesSent as number | undefined,
})
})
// Firefox does not report transport.selectedCandidatePairId: fall back to the
// nominated pair, then to the one that actually carried bytes.
let selected = working.find((pair) => pair.selected) ?? null
if (!selected) {
selected =
working.find((pair) => pair.nominated) ??
working
.slice()
.sort((a, b) => (b.bytesSent ?? 0) - (a.bytesSent ?? 0))[0] ??
null
if (selected) selected.selected = true
}
working.sort((a, b) => Number(b.selected) - Number(a.selected))
return { selected, working }
}
/**
* A synthetic track avoids asking for camera or microphone permission: this
* check must work for someone who denied both.
*/
const createProbeTrack = () => {
const canvas = document.createElement('canvas')
canvas.width = PROBE_WIDTH
canvas.height = PROBE_HEIGHT
const context = canvas.getContext('2d')
if (!context) throw new Error('Could not get canvas context')
let frame = 0
let rafId = 0
const draw = () => {
frame = (frame + 4) % 360
context.fillStyle = `hsl(${frame}, 100%, 50%)`
context.fillRect(0, 0, canvas.width, canvas.height)
rafId = requestAnimationFrame(draw)
}
draw()
const track = canvas.captureStream(PROBE_FPS).getVideoTracks()[0]
return {
track,
stop: () => {
cancelAnimationFrame(rafId)
track.stop()
},
}
}
export class SelectedCandidateCheck extends Checker {
private result: IceCandidateReport | null = null
get description() {
const selected = this.result?.selected
if (!selected) return 'Selected ICE candidate pair'
const transport =
selected.local.relayProtocol ?? selected.local.protocol ?? 'unknown'
const rtt =
selected.rttMs === undefined ? '' : ` · RTT ${selected.rttMs} ms`
return `${selected.local.type ?? 'unknown'} over ${transport}${rtt}`
}
protected async perform() {
await this.connect()
const probe = createProbeTrack()
try {
let publication
try {
publication = await this.room.localParticipant.publishTrack(
probe.track,
{
// The token restricts `can_publish_sources`, so a raw
// MediaStreamTrack published as `unknown` is rejected server side.
source: Track.Source.Camera,
simulcast: false,
videoEncoding: { maxBitrate: 300_000, maxFramerate: PROBE_FPS },
}
)
} catch (error) {
// A server-side grant problem is not a diagnosis of the user's network.
this.appendWarning(
`Could not publish the probe track: ${
error instanceof Error ? error.message : 'unknown error'
}`
)
this.skip()
return
}
// ICE keeps promoting pairs for a moment after the track goes up.
await new Promise((resolve) => setTimeout(resolve, SETTLE_DELAY_MS))
// Stats come from the publisher peer connection: in an empty test room
// there is no subscriber transport to inspect.
const report = await publication.track?.getRTCStatsReport()
this.result = report ? parseCandidates(report) : null
} finally {
probe.stop()
}
const selected = this.result?.selected
const working = this.result?.working ?? []
if (!selected) {
this.appendWarning('No working candidate pair reported by the browser')
return
}
this.appendMessage(`selected: ${describeCandidate(selected.local)}`)
this.appendMessage(`server: ${describeCandidate(selected.remote)}`)
if (selected.rttMs !== undefined) {
this.appendMessage(`round trip time: ${selected.rttMs} ms`)
}
this.appendMessage(`working candidate pairs: ${working.length}`)
for (const pair of working.slice(0, MAX_LOGGED_PAIRS)) {
const rtt = pair.rttMs === undefined ? '' : ` · ${pair.rttMs} ms`
this.appendMessage(
`${pair.selected ? '→' : ' '} ${describeCandidate(pair.local)}${describeCandidate(pair.remote)}${rtt}`
)
}
if (working.length > MAX_LOGGED_PAIRS) {
this.appendMessage(
`… and ${working.length - MAX_LOGGED_PAIRS} more, see the report`
)
}
if (selected.local.type === 'relay') {
this.appendWarning(
'Media is relayed through TURN. Direct connections are likely blocked by a firewall.'
)
}
if ((selected.local.relayProtocol ?? selected.local.protocol) !== 'udp') {
this.appendWarning(
'Media is not using UDP, which usually means degraded quality under load.'
)
}
}
getInfo(): CheckInfo {
const info = super.getInfo()
info.data = this.result ?? undefined
return info
}
}
@@ -1,186 +0,0 @@
import { useTranslation } from 'react-i18next'
import {
Disclosure,
DisclosurePanel,
Heading,
Button as RACButton,
} from 'react-aria-components'
import { RiArrowDownSFill } from '@remixicon/react'
import { css, cx } from '@/styled-system/css'
import type { ConnectionTestStepResult } from '../types'
import { StepStatusIndicator } from './StepStatusIndicator'
/** Each step is its own bounded card, collapsed or not. */
const cardClass = css({
border: '1px solid {colors.greyscale.900}',
borderRadius: '5px',
backgroundColor: 'white',
overflow: 'hidden',
})
/**
* Fixed columns so the status labels line up across every row, whether or not
* the row is expandable.
*/
const rowClass = css({
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) 7rem 1.5rem',
alignItems: 'center',
gap: '1rem',
width: '100%',
paddingX: '1rem',
paddingY: '0.75rem',
textAlign: 'left',
})
const identityClass = css({
display: 'flex',
flexDirection: 'column',
gap: '0.125rem',
minWidth: 0,
})
const triggerClass = css({
cursor: 'pointer',
transition: 'background-color 120ms',
_hover: { backgroundColor: 'greyscale.50' },
'&[data-focus-visible]': {
outline: '2px solid {colors.focusRing}',
outlineOffset: '-2px',
},
})
/** Expanded headers stay tinted so the open card reads as one block. */
const triggerExpandedClass = css({
backgroundColor: 'greyscale.100',
_hover: { backgroundColor: 'greyscale.100' },
})
const labelClass = css({
textStyle: 'body',
color: 'greyscale.1000',
fontWeight: 'medium',
})
const valueClass = css({
fontFamily: 'mono',
textStyle: 'xs',
color: 'greyscale.500',
overflowWrap: 'anywhere',
})
const chevronClass = css({
color: 'primary.800',
justifySelf: 'end',
transition: 'transform 150ms',
})
const chevronExpandedClass = css({ transform: 'rotate(180deg)' })
const headingResetClass = css({
margin: 0,
fontSize: 'inherit',
fontWeight: 'inherit',
})
const panelClass = css({
backgroundColor: 'white',
})
const logListClass = css({
listStyle: 'none',
margin: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
})
const logItemClass = css({
fontFamily: 'mono',
textStyle: 'xs',
color: 'greyscale.700',
overflowWrap: 'anywhere',
})
const StepRowContent = ({ step }: { step: ConnectionTestStepResult }) => {
const { t } = useTranslation('connectionTest')
return (
<>
<span className={identityClass}>
<span className={labelClass}>{t(`steps.${step.id}`)}</span>
{step.summary && <span className={valueClass}>{step.summary}</span>}
</span>
<StepStatusIndicator
status={step.status}
label={t(`status.${step.status}`)}
/>
</>
)
}
export const ConnectionTestStepRow = ({
step,
}: {
step: ConnectionTestStepResult
}) => {
const { t } = useTranslation('connectionTest')
const isSettled = step.status !== 'pending' && step.status !== 'running'
const hasLogs = isSettled && Boolean(step.logs?.length)
if (!hasLogs) {
return (
<div className={cx(cardClass, rowClass)}>
<StepRowContent step={step} />
{/* Empty chevron column keeps non-expandable rows aligned. */}
<span />
</div>
)
}
return (
<Disclosure className={cardClass}>
{({ isExpanded }) => (
<>
<Heading level={3} className={headingResetClass}>
<RACButton
slot="trigger"
className={cx(
rowClass,
triggerClass,
isExpanded ? triggerExpandedClass : undefined
)}
>
<StepRowContent step={step} />
<RiArrowDownSFill
aria-hidden="true"
className={cx(
chevronClass,
isExpanded ? chevronExpandedClass : undefined
)}
/>
</RACButton>
</Heading>
<DisclosurePanel
className={panelClass}
aria-label={t('detailsFor', { step: t(`steps.${step.id}`) })}
style={{ padding: isExpanded ? '0.75rem 1rem' : '0 1rem' }}
>
{/* Collapsed panels stay in the DOM for aria-controls, but the log
lines themselves are only mounted when actually visible. */}
{isExpanded && (
<ul className={logListClass}>
{step.logs?.map((log, index) => (
<li key={`${log.level}-${index}`} className={logItemClass}>
{log.message}
</li>
))}
</ul>
)}
</DisclosurePanel>
</>
)}
</Disclosure>
)
}
@@ -1,257 +0,0 @@
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { ProgressBar } from 'react-aria-components'
import { css, cx } from '@/styled-system/css'
import type { ConnectionTestStats } from '../types'
import { statusSquareClass } from './stepAppearance'
type SummaryState = 'idle' | 'running' | 'passed' | 'partial' | 'failed'
/** Only a failure earns a colour: everything else stays near-black. */
const stateColorClass: Record<SummaryState, string> = {
idle: css({ color: 'greyscale.1000' }),
running: css({ color: 'greyscale.1000' }),
passed: css({ color: 'greyscale.1000' }),
partial: css({ color: 'greyscale.1000' }),
failed: css({ color: 'danger.600' }),
}
const cardClass = css({
width: '100%',
borderRadius: '5px',
border: '1px solid {colors.greyscale.900}',
backgroundColor: 'white',
padding: { base: '1.25rem', xsm: '1.75rem' },
display: 'flex',
flexDirection: 'column',
// Blocks are spaced here; everything inside a block stays tight.
gap: '1.5rem',
})
const headerClass = css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
})
const eyebrowClass = css({
textStyle: 'sm',
fontWeight: 'medium',
color: 'greyscale.600',
margin: 0,
})
const headlineClass = css({
// Sized for the longest state string ("N vérifications en échec"), not for
// the shortest one.
fontSize: { base: '28', xsm: '40' },
lineHeight: '1.1',
fontWeight: 'bold',
letterSpacing: '-0.02em',
textWrap: 'balance',
margin: 0,
})
const hintClass = css({
textStyle: 'sm',
color: 'greyscale.600',
margin: 0,
maxWidth: '34rem',
})
const dividerClass = css({
// Lighter than the card border: an inner rule should never compete with it.
borderTop: '1px solid {colors.greyscale.100}',
paddingTop: '1.25rem',
display: 'flex',
flexDirection: 'column',
gap: '0.875rem',
})
const progressRowClass = css({
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
})
const trackClass = css({
height: '0.375rem',
width: '100%',
borderRadius: 'full',
backgroundColor: 'greyscale.200',
overflow: 'hidden',
})
const fillClass = css({
height: '100%',
borderRadius: 'full',
backgroundColor: 'primary.800',
transition: 'width 200ms ease-out',
})
const progressValueClass = css({
textStyle: 'sm',
fontVariantNumeric: 'tabular-nums',
color: 'greyscale.700',
whiteSpace: 'nowrap',
// Reserved width so the bar does not resize when the digits change.
minWidth: '3rem',
textAlign: 'right',
})
const countersClass = css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem 1.5rem',
})
const counterClass = css({
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
textStyle: 'sm',
color: 'greyscale.600',
})
const counterSquareClass = css({
width: '0.5rem',
height: '0.5rem',
borderRadius: '2px',
flexShrink: 0,
})
const counterValueClass = css({
fontWeight: 'medium',
fontVariantNumeric: 'tabular-nums',
color: 'greyscale.1000',
})
/** A zero count is context, not a result: it recedes instead of shouting. */
const emptyCounterClass = css({ color: 'greyscale.400' })
const emptySquareClass = css({
backgroundColor: 'transparent!',
border: '1px solid {colors.greyscale.250}',
})
const actionsClass = css({
display: 'flex',
flexWrap: 'wrap',
gap: '0.75rem',
})
const Counter = ({
squareClass,
value,
label,
}: {
squareClass: string
value: number
label: string
}) => {
const isEmpty = value === 0
return (
<span className={cx(counterClass, isEmpty ? emptyCounterClass : undefined)}>
<span
aria-hidden="true"
className={cx(
counterSquareClass,
squareClass,
isEmpty ? emptySquareClass : undefined
)}
/>
<span
className={cx(
counterValueClass,
isEmpty ? emptyCounterClass : undefined
)}
>
{value}
</span>
{label}
</span>
)
}
export const ConnectionTestSummary = ({
stats,
isRunning,
children,
}: {
stats: ConnectionTestStats
isRunning: boolean
children?: ReactNode
}) => {
const { t } = useTranslation('connectionTest')
const state: SummaryState = isRunning
? 'running'
: !stats.hasStarted
? 'idle'
: stats.failed > 0
? 'failed'
: stats.skipped > 0
? 'partial'
: 'passed'
return (
<section className={cardClass}>
<div className={headerClass}>
<h1 className={eyebrowClass}>{t('title')}</h1>
{/* Announced once per state change rather than on every step update. */}
<p className={cx(headlineClass, stateColorClass[state])} role="status">
{state === 'failed'
? t('summary.failed', { count: stats.failed })
: t(`summary.${state}`)}
</p>
<p className={hintClass}>{t(`summary.${state}Hint`)}</p>
</div>
{stats.hasStarted && (
<div className={dividerClass}>
<div className={progressRowClass}>
<ProgressBar
aria-label={t('progressLabel')}
value={stats.progress}
className={css({ flex: 1 })}
>
{({ percentage }) => (
<div className={trackClass}>
<div
className={fillClass}
style={{ width: `${percentage ?? 0}%` }}
/>
</div>
)}
</ProgressBar>
<span className={progressValueClass}>
{t('progress', { done: stats.settled, total: stats.total })}
</span>
</div>
<div className={countersClass}>
<Counter
squareClass={statusSquareClass.success}
value={stats.passed}
label={t('counts.passed')}
/>
<Counter
squareClass={statusSquareClass.skipped}
value={stats.skipped}
label={t('counts.skipped')}
/>
<Counter
squareClass={statusSquareClass.failed}
value={stats.failed}
label={t('counts.failed')}
/>
</div>
</div>
)}
{children && <div className={actionsClass}>{children}</div>}
</section>
)
}
@@ -1,40 +0,0 @@
import { css, cx } from '@/styled-system/css'
import type { ConnectionTestStepStatus } from '../types'
import { statusSquareClass, statusTextClass } from './stepAppearance'
const wrapperClass = css({
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
textStyle: 'sm',
whiteSpace: 'nowrap',
})
const squareClass = css({
width: '0.625rem',
height: '0.625rem',
borderRadius: '2px',
flexShrink: 0,
})
/**
* Status is carried by the label; the square is decorative so the meaning does
* not depend on colour alone.
*/
export const StepStatusIndicator = ({
status,
label,
className,
}: {
status: ConnectionTestStepStatus
label: string
className?: string
}) => (
<span className={cx(wrapperClass, className)}>
<span
aria-hidden="true"
className={cx(squareClass, statusSquareClass[status])}
/>
<span className={statusTextClass[status]}>{label}</span>
</span>
)
@@ -1,29 +0,0 @@
import { css } from '@/styled-system/css'
import type { ConnectionTestStepStatus } from '../types'
/**
* Panda extracts styles statically, so every status needs its own literal
* `css()` call: `css({ backgroundColor: someVariable })` would emit nothing.
*/
export const statusSquareClass: Record<ConnectionTestStepStatus, string> = {
pending: css({
backgroundColor: 'transparent',
border: '1px solid {colors.greyscale.300}',
}),
running: css({
backgroundColor: 'primary.800',
animation: 'pulse_background 1.2s ease-in-out infinite',
}),
success: css({ backgroundColor: 'success.600' }),
failed: css({ backgroundColor: 'danger.600' }),
skipped: css({ backgroundColor: 'greyscale.300' }),
}
/** Colour is carried by the square; the label stays near-black except on failure. */
export const statusTextClass: Record<ConnectionTestStepStatus, string> = {
pending: css({ color: 'greyscale.500' }),
running: css({ color: 'greyscale.700' }),
success: css({ color: 'greyscale.1000' }),
failed: css({ color: 'danger.600', fontWeight: 'medium' }),
skipped: css({ color: 'greyscale.500' }),
}
@@ -1,298 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import {
CheckStatus,
ConnectionCheck,
createLocalAudioTrack,
createLocalVideoTrack,
getBrowser,
type CheckInfo,
} from 'livekit-client'
import { fetchConnectionTestDetails } from '../api/fetchConnectionTestDetails'
import { SelectedCandidateCheck } from '../checks/selectedCandidate'
import {
createInitialSteps,
type ConnectionTestLog,
type ConnectionTestStepId,
type ConnectionTestStepResult,
type ConnectionTestStepStatus,
} from '../types'
import { openPermissionsDialog } from '@/stores/permissions'
const LIVEKIT_STEP_IDS: ConnectionTestStepId[] = [
'websocket',
'webrtc',
'turn',
'reconnect',
'selectedCandidate',
'publishAudio',
'publishVideo',
]
const CHECK_STATUS_TO_STEP: Record<CheckStatus, ConnectionTestStepStatus> = {
[CheckStatus.IDLE]: 'pending',
[CheckStatus.RUNNING]: 'running',
[CheckStatus.SUCCESS]: 'success',
[CheckStatus.FAILED]: 'failed',
[CheckStatus.SKIPPED]: 'skipped',
}
/** getUserMedia rejections that mean "the user said no", not "the device is broken". */
const PERMISSION_ERROR_NAMES = new Set([
'NotAllowedError',
'PermissionDeniedError',
'SecurityError',
])
const getErrorMessage = (error: unknown, fallback = 'Unknown error') =>
error instanceof Error ? error.message : fallback
const isPermissionError = (error: unknown) =>
error instanceof Error && PERMISSION_ERROR_NAMES.has(error.name)
const fromCheckInfo = (info: CheckInfo): Partial<ConnectionTestStepResult> => ({
status: CHECK_STATUS_TO_STEP[info.status] ?? 'failed',
summary: info.description,
logs: info.logs,
})
const groupDevicesByKind = (devices: MediaDeviceInfo[]) => {
const grouped: Record<string, string[]> = {
audioinput: [],
audiooutput: [],
videoinput: [],
}
for (const device of devices) {
// Browsers are free to report kinds we don't know about yet.
const bucket = (grouped[device.kind] ??= [])
bucket.push(device.label || device.deviceId)
}
return grouped
}
/**
* Outcome of a single step. `aborted` is deliberately distinct from `failed`:
* a cancelled run must not be reported to the user as a broken device.
*/
type StepOutcome =
| { state: 'success' }
| { state: 'failed'; error: unknown }
| { state: 'aborted' }
const ABORTED: StepOutcome = { state: 'aborted' }
export const useConnectionTestRunner = () => {
const [steps, setSteps] = useState(createInitialSteps)
const [isRunning, setIsRunning] = useState(false)
const abortRef = useRef<AbortController | null>(null)
const updateStep = useCallback(
(id: ConnectionTestStepId, patch: Partial<ConnectionTestStepResult>) => {
setSteps((current) =>
current.map((step) => (step.id === id ? { ...step, ...patch } : step))
)
},
[]
)
const skipSteps = useCallback(
(
ids: ConnectionTestStepId[],
summary: string,
logs?: ConnectionTestLog[]
) => {
// One state update for the whole batch instead of one per step.
const targets = new Set(ids)
setSteps((current) =>
current.map((step) =>
targets.has(step.id)
? { ...step, status: 'skipped', summary, logs }
: step
)
)
},
[]
)
const runStep = useCallback(
async (
id: ConnectionTestStepId,
signal: AbortSignal,
fn: () => Promise<Partial<ConnectionTestStepResult>>
): Promise<StepOutcome> => {
if (signal.aborted) return ABORTED
updateStep(id, {
status: 'running',
summary: undefined,
logs: undefined,
data: undefined,
})
try {
const result = await fn()
if (signal.aborted) return ABORTED
// `result.status` overrides when set (LiveKit checks map their own status)
updateStep(id, { status: 'success', ...result })
return { state: 'success' }
} catch (error) {
if (signal.aborted) return ABORTED
updateStep(id, {
status: 'failed',
summary: getErrorMessage(error),
})
return { state: 'failed', error }
}
},
[updateStep]
)
const runTest = useCallback(async () => {
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
const { signal } = controller
setIsRunning(true)
setSteps(createInitialSteps())
try {
await runStep('browser', signal, async () => {
const browser = getBrowser()
if (!browser) throw new Error('Browser not detected')
return {
summary: `${browser.name} ${browser.version}`,
data: {
name: browser.name,
version: browser.version,
os: browser.os,
osVersion: browser.osVersion,
},
}
})
if (signal.aborted) return
const microphone = await runStep('microphone', signal, async () => {
const track = await createLocalAudioTrack()
const label =
track.mediaStreamTrack.label ||
track.mediaStreamTrack.getSettings().deviceId ||
''
track.stop()
return { summary: label, data: { label } }
})
if (signal.aborted) return
if (
microphone.state === 'failed' &&
isPermissionError(microphone.error)
) {
openPermissionsDialog('audioinput')
}
const camera = await runStep('camera', signal, async () => {
const track = await createLocalVideoTrack()
const settings = track.mediaStreamTrack.getSettings()
const label = track.mediaStreamTrack.label || ''
// Released immediately, like the microphone probe: the capture
// indicator must not stay on between this check and publishVideo.
track.stop()
return {
summary: label,
data: {
label,
width: settings.width,
height: settings.height,
},
}
})
if (signal.aborted) return
if (camera.state === 'failed' && isPermissionError(camera.error)) {
openPermissionsDialog('videoinput')
}
await runStep('devices', signal, async () => {
const devices = await navigator.mediaDevices.enumerateDevices()
return {
summary: String(devices.length),
data: groupDevicesByKind(devices),
}
})
if (signal.aborted) return
let checker: ConnectionCheck
try {
const { livekit } = await fetchConnectionTestDetails()
if (signal.aborted) return
checker = new ConnectionCheck(livekit.url, livekit.token)
} catch (error) {
if (signal.aborted) return
skipSteps(
LIVEKIT_STEP_IDS,
getErrorMessage(error, 'Failed to fetch test token')
)
return
}
// LiveKit's ConnectionCheck exposes no cancellation: each check owns its
// own room and disconnects it when it settles. The best we can do is
// never start the next one once the run has been aborted (runStep
// short-circuits on `signal.aborted`).
await runStep('websocket', signal, async () =>
fromCheckInfo(await checker.checkWebsocket())
)
await runStep('webrtc', signal, async () =>
fromCheckInfo(await checker.checkWebRTC())
)
await runStep('turn', signal, async () =>
fromCheckInfo(await checker.checkTURN())
)
await runStep('reconnect', signal, async () =>
fromCheckInfo(await checker.checkReconnect())
)
await runStep('selectedCandidate', signal, async () =>
fromCheckInfo(await checker.createAndRunCheck(SelectedCandidateCheck))
)
if (microphone.state !== 'success') {
skipSteps(['publishAudio'], 'Microphone permission required')
} else {
await runStep('publishAudio', signal, async () =>
fromCheckInfo(await checker.checkPublishAudio())
)
}
if (camera.state !== 'success') {
skipSteps(['publishVideo'], 'Camera permission required')
} else {
await runStep('publishVideo', signal, async () =>
fromCheckInfo(await checker.checkPublishVideo())
)
}
} finally {
if (!signal.aborted) {
setIsRunning(false)
}
}
}, [runStep, skipSteps])
const reset = useCallback(() => {
abortRef.current?.abort()
setSteps(createInitialSteps())
setIsRunning(false)
}, [])
// Leaving the page mid-run must stop the pending checks rather than let them
// keep a LiveKit session open behind an unmounted component.
useEffect(
() => () => {
abortRef.current?.abort()
},
[]
)
return {
steps,
isRunning,
runTest,
reset,
}
}
@@ -1,167 +0,0 @@
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCloseLine,
RiDownload2Line,
RiErrorWarningLine,
RiPlayLine,
} from '@remixicon/react'
import { CenteredContent } from '@/layout/CenteredContent'
import { Screen } from '@/layout/Screen'
import { Button } from '@/primitives'
import { css } from '@/styled-system/css'
import { Center, VStack } from '@/styled-system/jsx'
import { Permissions } from '@/features/rooms/components/Permissions'
import { useConnectionTestRunner } from '../hooks/useConnectionTestRunner'
import { ConnectionTestStepRow } from '../components/ConnectionTestStepRow'
import { ConnectionTestSummary } from '../components/ConnectionTestSummary'
import { CONNECTION_TEST_GROUPS, summarizeSteps } from '../types'
import { downloadConnectionTestReport } from '../utils/downloadConnectionTestReport'
import { useConfig } from '@/api/useConfig'
import { navigateTo } from '@/navigation/navigateTo'
const HIDE_LIVEKIT_VIDEO_CLASS = 'connection-test-hide-livekit-video'
const sectionClass = css({
width: '100%',
borderTop: '2px solid {colors.greyscale.900}',
paddingTop: '1rem',
})
const sectionTitleClass = css({
textStyle: 'h2',
color: 'greyscale.1000',
margin: 0,
})
const rowsClass = css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
marginTop: '0.75rem',
})
const helpClass = css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.5rem',
width: '100%',
borderRadius: 8,
border: '1px solid {colors.greyscale.200}',
backgroundColor: 'white',
padding: '0.75rem 1rem',
textStyle: 'sm',
color: 'greyscale.800',
})
const helpIconClass = css({
color: 'danger.600',
flexShrink: 0,
marginTop: '2px',
})
const ConnectionTest = () => {
const { data, isLoading } = useConfig()
const { t } = useTranslation('connectionTest')
const { steps, isRunning, runTest, reset } = useConnectionTestRunner()
const stats = useMemo(() => summarizeSteps(steps), [steps])
const stepsById = useMemo(
() => new Map(steps.map((step) => [step.id, step] as const)),
[steps]
)
const isPublishVideoRunning =
stepsById.get('publishVideo')?.status === 'running'
// LiveKit appends a bare <video> to document.body during publishVideo.
// Keep it in the DOM (so the frame check still works) but hide it visually.
useEffect(() => {
document.body.classList.toggle(
HIDE_LIVEKIT_VIDEO_CLASS,
isPublishVideoRunning
)
return () => {
document.body.classList.remove(HIDE_LIVEKIT_VIDEO_CLASS)
}
}, [isPublishVideoRunning])
useEffect(() => {
// Wait for config to load, otherwise we'd redirect off a page
// that's actually enabled.
if (!isLoading && !data?.diagnostics?.connection_test_enabled) {
navigateTo('home', undefined, { replace: true })
}
}, [isLoading, data])
return (
<Screen layout="centered">
<Permissions />
<CenteredContent withBackButton>
<Center>
<VStack gap="1.5rem" maxWidth="40rem" width="100%">
<ConnectionTestSummary stats={stats} isRunning={isRunning}>
{isRunning ? (
// A disabled "run" button while the test runs is dead weight:
// cancelling is the only thing left to do.
<Button
variant="secondary"
onPress={reset}
icon={<RiCloseLine size={18} aria-hidden="true" />}
>
{t('cancel')}
</Button>
) : (
<Button
variant="primary"
onPress={runTest}
icon={<RiPlayLine size={18} aria-hidden="true" />}
>
{stats.hasStarted ? t('runAgain') : t('runTest')}
</Button>
)}
{stats.hasStarted && !isRunning && (
<Button
variant="secondary"
onPress={() => downloadConnectionTestReport(steps)}
icon={<RiDownload2Line size={18} aria-hidden="true" />}
>
{t('downloadReport')}
</Button>
)}
</ConnectionTestSummary>
{stats.hasStarted &&
CONNECTION_TEST_GROUPS.map((group) => (
<section key={group.id} className={sectionClass}>
<h2 className={sectionTitleClass}>
{t(`groups.${group.id}`)}
</h2>
<div className={rowsClass}>
{group.steps.map((id) => {
const step = stepsById.get(id)
return step ? (
<ConnectionTestStepRow key={id} step={step} />
) : null
})}
</div>
</section>
))}
{stats.failed > 0 && !isRunning && (
<p className={helpClass}>
<RiErrorWarningLine
size={18}
aria-hidden="true"
className={helpIconClass}
/>
{t('help.firewall')}
</p>
)}
</VStack>
</Center>
</CenteredContent>
</Screen>
)
}
export default ConnectionTest
@@ -1,103 +0,0 @@
export type ConnectionTestStepId =
| 'browser'
| 'microphone'
| 'camera'
| 'devices'
| 'websocket'
| 'webrtc'
| 'turn'
| 'reconnect'
| 'selectedCandidate'
| 'publishAudio'
| 'publishVideo'
export type ConnectionTestStepStatus =
| 'pending'
| 'running'
| 'success'
| 'failed'
| 'skipped'
export type ConnectionTestLog = {
level: 'info' | 'warning' | 'error'
message: string
}
export type ConnectionTestStepResult = {
id: ConnectionTestStepId
status: ConnectionTestStepStatus
summary?: string
logs?: ConnectionTestLog[]
data?: Record<string, unknown>
}
export type ConnectionTestGroupId = 'local' | 'network'
/** Display order: everything local first, then everything that leaves the machine. */
export const CONNECTION_TEST_GROUPS: ReadonlyArray<{
id: ConnectionTestGroupId
steps: ReadonlyArray<ConnectionTestStepId>
}> = [
{ id: 'local', steps: ['browser', 'microphone', 'camera', 'devices'] },
{
id: 'network',
steps: [
'websocket',
'webrtc',
'turn',
'reconnect',
'selectedCandidate',
'publishAudio',
'publishVideo',
],
},
]
export const CONNECTION_TEST_STEP_IDS: ConnectionTestStepId[] =
CONNECTION_TEST_GROUPS.flatMap((group) => [...group.steps])
export const createInitialSteps = (): ConnectionTestStepResult[] =>
CONNECTION_TEST_STEP_IDS.map((id) => ({ id, status: 'pending' }))
export type ConnectionTestStats = {
total: number
settled: number
passed: number
failed: number
skipped: number
hasStarted: boolean
progress: number
}
/**
* Single pass over the steps: the page needs half a dozen derived booleans and
* counters, and scanning the array once per render beats one `.some()` per flag.
*/
export const summarizeSteps = (
steps: ConnectionTestStepResult[]
): ConnectionTestStats => {
let passed = 0
let failed = 0
let skipped = 0
let pending = 0
for (const step of steps) {
if (step.status === 'success') passed += 1
else if (step.status === 'failed') failed += 1
else if (step.status === 'skipped') skipped += 1
else if (step.status === 'pending') pending += 1
}
const total = steps.length
const settled = passed + failed + skipped
return {
total,
settled,
passed,
failed,
skipped,
hasStarted: pending < total,
progress: total === 0 ? 0 : Math.round((settled / total) * 100),
}
}
@@ -1,53 +0,0 @@
import type { ConnectionTestStepResult } from '../types'
export type ConnectionTestReport = {
generatedAt: string
userAgent: string
steps: Record<
string,
{
status: ConnectionTestStepResult['status']
summary?: string
logs?: ConnectionTestStepResult['logs']
data?: ConnectionTestStepResult['data']
}
>
}
export const buildConnectionTestReport = (
steps: ConnectionTestStepResult[]
): ConnectionTestReport => ({
generatedAt: new Date().toISOString(),
userAgent: navigator.userAgent,
steps: Object.fromEntries(
steps.map(({ id, status, summary, logs, data }) => [
id,
{
status,
...(summary !== undefined ? { summary } : {}),
...(logs?.length ? { logs } : {}),
...(data !== undefined ? { data } : {}),
},
])
),
})
export const downloadConnectionTestReport = (
steps: ConnectionTestStepResult[]
) => {
const report = buildConnectionTestReport(steps)
const timestamp = report.generatedAt.slice(0, 19).replace(/:/g, '-')
const blob = new Blob([JSON.stringify(report, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `connection-test-${timestamp}.json`
// Firefox only follows the click when the anchor is in the document, and
// revoking the URL in the same tick cancels the download in some browsers.
document.body.appendChild(anchor)
anchor.click()
anchor.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
@@ -98,7 +98,6 @@ export const NoAccessView = ({
)}
</Text>
</VStack>
{/*todo - update the message here to adapt to room configuration */}
{!isLoggedIn && (
<LoginPrompt
heading={t(`${i18nKey}.login.heading`)}
@@ -29,7 +29,6 @@ import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums'
import { LimitDescription } from './LimitDescription'
import { useCanRecord } from '@/features/recording/hooks/useCanRecord'
export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
@@ -39,7 +38,6 @@ export const ScreenRecordingSidePanel = () => {
const [includeTranscript, setIncludeTranscript] = useState(false)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
const hasScreenRecordingAccess = useHasRecordingAccess(
@@ -112,7 +110,7 @@ export const ScreenRecordingSidePanel = () => {
}
}
if (!canRecord) {
if (!isAdminOrOwner) {
return (
<NoAccessView
i18nKeyPrefix={keyPrefix}
@@ -6,7 +6,7 @@ import { useRoomContext } from '@livekit/components-react'
import {
RecordingMode,
useHasRecordingAccess,
useHasFeatureWithoutRecordingRights,
useHasFeatureWithoutAdminRights,
useRecordingStatuses,
} from '../index'
import { useState } from 'react'
@@ -53,7 +53,7 @@ export const TranscriptSidePanel = () => {
FeatureFlags.Transcript
)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutRecordingRights(
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
@@ -1,15 +0,0 @@
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useUser } from '@/features/auth/api/useUser'
export const useCanRecord = () => {
const apiRoomData = useRoomData()
const isAdminOrOwner = useIsAdminOrOwner()
const { isLoggedIn } = useUser()
return (
isAdminOrOwner ||
(!!isLoggedIn &&
apiRoomData?.configuration?.authenticated_can_record !== false)
)
}
@@ -2,21 +2,21 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useCanRecord } from './useCanRecord'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import type { FeatureFlags } from '@/features/analytics/enums'
export const useHasFeatureWithoutRecordingRights = (
export const useHasFeatureWithoutAdminRights = (
mode: RecordingMode,
featureFlag: FeatureFlags
) => {
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isRecordingModeEnabled &&
!canRecord
!isAdminOrOwner
)
}
@@ -2,8 +2,8 @@ import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import type { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import type { FeatureFlags } from '@/features/analytics/enums'
import { useCanRecord } from './useCanRecord'
export const useHasRecordingAccess = (
mode: RecordingMode,
@@ -12,11 +12,11 @@ export const useHasRecordingAccess = (
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const canRecord = useCanRecord()
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
canRecord &&
isAdminOrOwner &&
isRecordingModeEnabled
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
// hooks
export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useHasFeatureWithoutRecordingRights } from './hooks/useHasFeatureWithoutRecordingRights.ts'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
export { useRecordingStatuses } from './hooks/useRecordingStatuses'
// api
+3 -17
View File
@@ -16,15 +16,6 @@ export enum ApiAccessLevel {
export type RoomConfiguration = {
can_publish_sources?: Source[] | null
everyone_can_mute?: boolean | null
authenticated_can_record?: boolean | null
}
export type ParticipantRole = 'member' | 'administrator' | 'owner'
export type AssignableParticipantRole = Exclude<ParticipantRole, 'owner'>
export type ApiResourceAccess = {
id: string
role: ParticipantRole
}
export type ApiRoom = {
@@ -36,12 +27,7 @@ export type ApiRoom = {
access_level: ApiAccessLevel
livekit?: ApiLiveKit
configuration?: RoomConfiguration
/**
* Only present in the API response when the requesting user is an
* administrator or owner of the room (see RoomSerializer.to_representation
* in the backend). Its presence can therefore be used to detect
* administrability outside of a LiveKit session, where the room_role
* participant attribute is not available.
*/
accesses?: ApiResourceAccess[]
}
export type ParticipantRole = 'member' | 'administrator' | 'owner'
export type AssignableParticipantRole = Exclude<ParticipantRole, 'owner'>
@@ -3,12 +3,12 @@ import { fetchApi } from '@/api/fetchApi'
export const fetchRoom = ({
roomId,
username,
username = '',
}: {
roomId: string
username?: string
}) => {
const query = username ? `?username=${encodeURIComponent(username)}` : ''
return fetchApi<ApiRoom>(`/rooms/${roomId}${query}`)
return fetchApi<ApiRoom>(
`/rooms/${roomId}?username=${encodeURIComponent(username)}`
)
}
@@ -2,8 +2,6 @@ import { type ApiRoom } from './ApiRoom'
import { fetchApi } from '@/api/fetchApi'
import { useMutation, type UseMutationOptions } from '@tanstack/react-query'
import type { ApiError } from '@/api/ApiError'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
export type PatchRoomParams = {
roomId: string
@@ -17,25 +15,11 @@ export const patchRoom = ({ roomId, room }: PatchRoomParams) => {
})
}
export const patchRoomMutationKey = ['patchRoom']
export function usePatchRoom(
options?: UseMutationOptions<ApiRoom, ApiError, PatchRoomParams>
) {
return useMutation<ApiRoom, ApiError, PatchRoomParams>({
mutationKey: patchRoomMutationKey,
mutationFn: patchRoom,
onMutate: async ({ roomId, room: partialRoom }) => {
await queryClient.cancelQueries({ queryKey: [keys.room, roomId] })
queryClient.setQueryData<ApiRoom>([keys.room, roomId], (previous) =>
previous ? { ...previous, ...partialRoom } : previous
)
},
onSettled: (_data, _error, { roomId }) => {
if (queryClient.isMutating({ mutationKey: patchRoomMutationKey }) === 1) {
queryClient.invalidateQueries({ queryKey: [keys.room, roomId] })
}
},
...options,
onSuccess: options?.onSuccess,
})
}
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
import { useQuery } from '@tanstack/react-query'
import { useParams } from 'wouter'
@@ -49,12 +50,7 @@ export const Admin = () => {
isScreenShareEnabled,
} = usePublishSourcesManager()
const {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
} = usePermissionsManager()
const { toggleMuting, isMutingEnabled } = usePermissionsManager()
return (
<Div
@@ -159,17 +155,6 @@ export const Admin = () => {
fullWidth: true,
}}
/>
<Field
type="switch"
label={'Authenticated can record'}
description={'Authenticated can record'}
isSelected={isAuthenticatedCanRecordEnabled}
onChange={toggleAuthenticatedCanRecord}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</div>
</div>
<div
@@ -221,7 +206,11 @@ export const Admin = () => {
patchRoom({
roomId,
room: { access_level: value as ApiAccessLevel },
}).catch((e) => console.error(e))
})
.then((room) => {
queryClient.setQueryData([keys.room, roomId], room)
})
.catch((e) => console.error(e))
}
items={[
{
@@ -1,6 +1,8 @@
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { useRoomData } from '@/features/rooms/livekit/hooks/useRoomData'
import { useCallback } from 'react'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
export const usePermissionsManager = () => {
const { mutateAsync: patchRoom } = usePatchRoom()
@@ -10,8 +12,6 @@ export const usePermissionsManager = () => {
const roomId = data?.slug
const isMutingEnabled = configuration?.everyone_can_mute ?? true
const isAuthenticatedCanRecordEnabled =
configuration?.authenticated_can_record ?? true
const toggleMuting = useCallback(
async (enabled: boolean) => {
@@ -23,11 +23,13 @@ export const usePermissionsManager = () => {
everyone_can_mute: enabled,
}
await patchRoom({
const room = await patchRoom({
roomId,
room: { configuration: newConfiguration },
})
queryClient.setQueryData([keys.room, roomId], room)
return { configuration: newConfiguration }
} catch (error) {
console.error('Failed to update muting permission:', error)
@@ -37,36 +39,8 @@ export const usePermissionsManager = () => {
[configuration, roomId, patchRoom]
)
const toggleAuthenticatedCanRecord = useCallback(
async (enabled: boolean) => {
if (!roomId) return
try {
const newConfiguration = {
...configuration,
authenticated_can_record: enabled,
}
const room = await patchRoom({
roomId,
room: { configuration: newConfiguration },
})
queryClient.setQueryData([keys.room, roomId], room)
return { configuration: newConfiguration }
} catch (error) {
console.error('Failed to update recording permission:', error)
return { success: false, error }
}
},
[configuration, roomId, patchRoom]
)
return {
toggleMuting,
isMutingEnabled,
isAuthenticatedCanRecordEnabled,
toggleAuthenticatedCanRecord,
}
}
@@ -1,5 +1,7 @@
import { RoomEvent, Track } from 'livekit-client'
import { useCallback, useMemo } from 'react'
import { queryClient } from '@/api/queryClient'
import { keys } from '@/api/queryKeys'
import { useConfig } from '@/api/useConfig'
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { useRemoteParticipants } from '@livekit/components-react'
@@ -77,11 +79,13 @@ export const usePublishSourcesManager = () => {
can_publish_sources: newSources,
}
await patchRoom({
const room = await patchRoom({
roomId,
room: { configuration: newConfiguration },
})
queryClient.setQueryData([keys.room, roomId], room)
await updateParticipantsPermissions(
unprivilegedRemoteParticipants,
newSources
@@ -4,7 +4,7 @@ import { Link } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { HStack, VStack } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { RiCloseLine, RiFileCopyLine, RiSettings3Line } from '@remixicon/react'
import { RiCloseLine, RiFileCopyLine } from '@remixicon/react'
import { Text } from '@/primitives'
import { Spinner } from '@/primitives/Spinner'
import { buttonRecipe } from '@/primitives/buttonRecipe'
@@ -37,49 +37,15 @@ const CreateMeetingButton = () => {
initialRoom
)
const [isRoomCreatedInSession, setIsRoomCreatedInSession] = useState(false)
const showSettingsButton =
isRoomCreatedInSession || searchParams.get('settings') === 'true'
const { data } = useRoomCreationCallback({ callbackId })
const roomUrl = useMemo(() => {
if (room?.slug) return getRouteUrl('room', room.slug)
}, [room])
const backgroundColor = useMemo(() => {
const param = searchParams.get('backgroundColor')
if (!param) return 'transparent'
const value = param.trim()
// Allow raw hex passed without '#' (e.g. ?backgroundColor=ff0000)
if (
/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{4}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(
value
)
) {
return `#${value}`
}
// Already-valid hex (e.g. URL-encoded %23ff0000 → '#ff0000')
if (/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) {
return value
}
// Fallback: only allow simple named colors, block anything injectable
if (/^[a-zA-Z]+$/.test(value)) {
return value
}
return 'transparent'
}, [searchParams])
useEffect(() => {
if (!data?.room?.slug) return
setRoom(data.room)
setIsRoomCreatedInSession(true)
setCallbackId(undefined)
setIsPending(false)
popupManager.sendRoomData({
@@ -95,7 +61,6 @@ const CreateMeetingButton = () => {
(id) => setCallbackId(id),
(data) => {
setRoom(data)
setIsRoomCreatedInSession(true)
setIsPending(false)
}
)
@@ -105,7 +70,6 @@ const CreateMeetingButton = () => {
const resetState = () => {
setRoom(undefined)
setIsRoomCreatedInSession(false)
setCallbackId(undefined)
setIsPending(false)
popupManager.clearState()
@@ -114,27 +78,20 @@ const CreateMeetingButton = () => {
if (isPending) {
return (
<div
style={{
backgroundColor: backgroundColor,
height: '100%',
}}
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
})}
>
<Spinner size={34} />
<Button
variant="quaternaryText"
square
icon={<RiCloseLine />}
onPress={resetState}
aria-label={t('resetLabel')}
/>
</div>
<Spinner size={34} />
<Button
variant="quaternaryText"
square
icon={<RiCloseLine />}
onPress={resetState}
aria-label={t('resetLabel')}
/>
</div>
)
}
@@ -147,8 +104,6 @@ const CreateMeetingButton = () => {
justifyContent: 'start',
alignItems: 'start',
border: 'none',
backgroundColor: backgroundColor,
height: '100%',
}}
>
{roomUrl && room?.slug ? (
@@ -166,25 +121,14 @@ const CreateMeetingButton = () => {
{t('joinButton')}
</Link>
<HStack gap={0}>
{showSettingsButton && (
<Button
variant="quaternaryText"
square
icon={<RiSettings3Line />}
aria-label={t('settingsTooltip')}
onPress={() => {
popupManager.createSettingsPopupWindow(room.slug, () => {})
}}
/>
)}
<Button
variant="quaternaryText"
square
icon={<RiFileCopyLine />}
tooltip={t('copyLinkTooltip')}
onPress={() => {
navigator.clipboard.writeText(roomUrl)
}}
aria-label={t('copyLinkTooltip')}
/>
{searchParams.get('readOnly') === 'false' && (
<Button
@@ -1,374 +0,0 @@
import { useEffect, useMemo, type ReactNode } from 'react'
import { useSearchParams } from 'wouter'
import { useTranslation } from 'react-i18next'
import { useQuery } from '@tanstack/react-query'
import { Track } from 'livekit-client'
import { css } from '@/styled-system/css'
import { Button, Field, H, Text } from '@/primitives'
import { Spinner } from '@/primitives/Spinner'
import { keys } from '@/api/queryKeys'
import { useConfig } from '@/api/useConfig'
import { useUser } from '@/features/auth/api/useUser'
import { authUrl } from '@/features/auth/utils/authUrl'
import { fetchRoom } from '@/features/rooms/api/fetchRoom'
import { usePatchRoom } from '@/features/rooms/api/patchRoom'
import { ApiAccessLevel } from '@/features/rooms/api/ApiRoom'
import { updatePublishSources } from '@/features/rooms/livekit/hooks/usePublishSourcesManager'
import { isSubsetOf } from '@/features/rooms/utils/isSubsetOf'
type Source = Track.Source
const SectionHeader = ({ children }: { children: ReactNode }) => (
<div
className={css({
backgroundColor: 'greyscale.50',
borderTopWidth: '1px',
borderTopStyle: 'solid',
borderTopColor: 'greyscale.250',
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: 'greyscale.250',
padding: '0.75rem 1.5rem',
})}
>
<H
lvl={2}
margin={false}
className={css({
fontWeight: 500,
fontSize: '1.125rem',
})}
>
{children}
</H>
</div>
)
const SectionBody = ({ children }: { children: ReactNode }) => (
<div
className={css({
display: 'flex',
flexDirection: 'column',
padding: '1rem 1.5rem 1.5rem',
})}
>
{children}
</div>
)
const SettingsPopup = () => {
const { t } = useTranslation('sdk', { keyPrefix: 'roomSettings' })
const { t: tRooms } = useTranslation('rooms', { keyPrefix: 'admin' })
const [searchParams] = useSearchParams()
const roomSlug = searchParams.get('slug')?.trim()
const { isLoggedIn } = useUser({ fetchUserOptions: { attemptSilent: false } })
useEffect(() => {
if (isLoggedIn === false) {
// returnTo defaults to the current URL, so the user comes back to this
// popup (with the slug preserved) once authentication completes.
window.location.href = authUrl({})
}
}, [isLoggedIn])
const {
data: room,
isLoading,
isError,
} = useQuery({
queryKey: [keys.room, roomSlug],
queryFn: () => fetchRoom({ roomId: roomSlug as string }),
enabled: !!isLoggedIn && !!roomSlug,
retry: false,
})
const { mutateAsync: patchRoom } = usePatchRoom()
const { data: configData } = useConfig()
const configuration = room?.configuration
const currentSources = useMemo(() => {
const defaultSources = configData?.livekit?.default_sources ?? []
if (
configuration?.can_publish_sources == undefined ||
!Array.isArray(configuration?.can_publish_sources)
) {
return defaultSources
}
return configuration.can_publish_sources
}, [configData, configuration?.can_publish_sources])
const patchConfiguration = (
newConfiguration: NonNullable<typeof configuration>
) => {
if (!roomSlug) return
patchRoom({
roomId: roomSlug,
room: { configuration: newConfiguration },
}).catch((e) => console.error(e))
}
const updateSource = (sources: Source[], enabled: boolean) => {
patchConfiguration({
...configuration,
can_publish_sources: updatePublishSources(
currentSources,
sources,
enabled
),
})
}
const toggleMicrophone = (enabled: boolean) =>
updateSource([Track.Source.Microphone], enabled)
const toggleCamera = (enabled: boolean) =>
updateSource([Track.Source.Camera], enabled)
const toggleScreenShare = (enabled: boolean) =>
updateSource(
[Track.Source.ScreenShare, Track.Source.ScreenShareAudio],
enabled
)
const toggleMuting = (enabled: boolean) =>
patchConfiguration({
...configuration,
everyone_can_mute: enabled,
})
const isMicrophoneEnabled = isSubsetOf(
[Track.Source.Microphone],
currentSources
)
const isCameraEnabled = isSubsetOf([Track.Source.Camera], currentSources)
const isScreenShareEnabled = isSubsetOf(
[Track.Source.ScreenShare, Track.Source.ScreenShareAudio],
currentSources
)
const isMutingEnabled = configuration?.everyone_can_mute ?? true
const renderCentered = (children: ReactNode) => (
<div
className={css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
width: '100%',
padding: '1.5rem',
})}
>
{children}
</div>
)
if (!roomSlug || isError) {
return renderCentered(
<Text variant="note" margin={false}>
{t('error')}
</Text>
)
}
if (isLoggedIn === undefined || isLoggedIn === false || isLoading || !room) {
return renderCentered(<Spinner />)
}
const isAdministrable = room.accesses !== undefined
if (!isAdministrable) {
return renderCentered(
<Text variant="note" margin={false}>
{t('notAllowed')}
</Text>
)
}
return (
<div
className={css({
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
minHeight: 0,
})}
>
<header
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '0.5rem',
padding: '1.5rem',
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: 'greyscale.250',
})}
>
<img
src="/assets/logo.svg"
alt=""
className={css({
maxHeight: '40px',
flexShrink: 0,
})}
/>
<div className={css({ display: 'flex', flexDirection: 'column' })}>
<H
lvl={1}
margin={false}
className={css({
fontWeight: 500,
})}
>
{t('title')}
</H>
<Text variant="smNote" margin={false}>
{roomSlug}
</Text>
</div>
</header>
<div
className={css({
flexGrow: 1,
overflowY: 'auto',
minHeight: 0,
})}
>
<SectionHeader>{tRooms('moderation.title')}</SectionHeader>
<SectionBody>
<Text
variant="note"
wrap="balance"
className={css({
textStyle: 'sm',
})}
margin={'md'}
>
{tRooms('moderation.description')}
</Text>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
})}
>
<Field
type="switch"
label={tRooms('moderation.microphone.label')}
description={tRooms('moderation.microphone.description')}
isSelected={isMicrophoneEnabled}
onChange={toggleMicrophone}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
<Field
type="switch"
label={tRooms('moderation.camera.label')}
description={tRooms('moderation.camera.description')}
isSelected={isCameraEnabled}
onChange={toggleCamera}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
<Field
type="switch"
label={tRooms('moderation.screenshare.label')}
description={tRooms('moderation.screenshare.description')}
isSelected={isScreenShareEnabled}
onChange={toggleScreenShare}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
<Field
type="switch"
label={tRooms('moderation.mute.label')}
description={tRooms('moderation.mute.description')}
isSelected={isMutingEnabled}
onChange={toggleMuting}
wrapperProps={{
noMargin: true,
fullWidth: true,
}}
/>
</div>
</SectionBody>
<SectionHeader>{tRooms('access.title')}</SectionHeader>
<SectionBody>
<Text
variant="note"
wrap="balance"
className={css({
textStyle: 'sm',
})}
margin={'md'}
>
{tRooms('access.description')}
</Text>
<Field
type="radioGroup"
label={tRooms('access.type')}
aria-label={tRooms('access.type')}
labelProps={{
className: css({
fontSize: '1rem',
paddingBottom: '1rem',
}),
}}
value={room.access_level}
onChange={(value) =>
patchRoom({
roomId: roomSlug,
room: { access_level: value as ApiAccessLevel },
}).catch((e) => console.error(e))
}
items={[
{
value: ApiAccessLevel.PUBLIC,
label: tRooms('access.levels.public.label'),
description: tRooms('access.levels.public.description'),
},
{
value: ApiAccessLevel.TRUSTED,
label: tRooms('access.levels.trusted.label'),
description: tRooms('access.levels.trusted.description'),
},
{
value: ApiAccessLevel.RESTRICTED,
label: tRooms('access.levels.restricted.label'),
description: tRooms('access.levels.restricted.description'),
},
]}
/>
</SectionBody>
</div>
<footer
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
padding: '1rem 1.5rem',
borderTopWidth: '1px',
borderTopStyle: 'solid',
borderTopColor: 'greyscale.250',
})}
>
<Button size="sm" onPress={() => window.close()}>
{t('closeButton')}
</Button>
</footer>
</div>
)
}
export default SettingsPopup
@@ -24,20 +24,6 @@ export class PopupManager {
}
}
public createSettingsPopupWindow(roomSlug: string, onFailure: () => void) {
const popupWindow = window.open(
`${window.location.origin}/sdk/settings-popup?slug=${encodeURIComponent(roomSlug)}`,
'SettingsPopupWindow',
`status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes`
)
if (popupWindow) {
popupWindow.focus()
} else {
onFailure()
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private messageParent(type: ClientMessageType, data: any) {
window?.parent.postMessage(
-12
View File
@@ -266,18 +266,6 @@ export const Footer = () => {
{t('links.accessibility')}
</Link>
</StyledLi>
{data?.diagnostics?.connection_test_enabled && (
<StyledLi divider>
<Link
underline={false}
footer="minor"
to="/test-connection"
aria-label={t('links.connectionTest')}
>
{t('links.connectionTest')}
</Link>
</StyledLi>
)}
<StyledLi>
<A
externalIcon
@@ -1,56 +0,0 @@
{
"title": "Ihre Konfiguration testen",
"runTest": "Test starten",
"runAgain": "Erneut testen",
"cancel": "Abbrechen",
"detailsFor": "Details zu Schritt {{step}}",
"downloadReport": "Bericht herunterladen",
"homeLink": "Ihre Konfiguration testen",
"progress": "{{done}}/{{total}}",
"progressLabel": "Fortschritt des Verbindungstests",
"groups": {
"local": "Browser und Geräte",
"network": "Verbindung zum Server"
},
"steps": {
"browser": "Browser",
"microphone": "Mikrofon",
"camera": "Kamera",
"devices": "Mediengeräte",
"websocket": "WebSocket",
"webrtc": "WebRTC",
"turn": "TURN",
"reconnect": "Erneut verbinden",
"selectedCandidate": "Ausgewählte Route",
"publishAudio": "Audio veröffentlichen",
"publishVideo": "Video veröffentlichen"
},
"status": {
"pending": "Ausstehend",
"running": "Läuft…",
"success": "Erfolgreich",
"failed": "Fehlgeschlagen",
"skipped": "Übersprungen"
},
"counts": {
"passed": "erfolgreich",
"failed": "fehlgeschlagen",
"skipped": "übersprungen"
},
"summary": {
"idle": "Bereit zum Testen",
"idleHint": "Der Test dauert ungefähr eine Minute. Ihre Kamera und Ihr Mikrofon werden nur während des Tests verwendet.",
"running": "Test wird durchgeführt…",
"runningHint": "Lassen Sie diese Seite geöffnet, bis alle Prüfungen abgeschlossen sind.",
"passed": "Alles funktioniert",
"passedHint": "Ihr Browser, Ihre Geräte und Ihr Netzwerk sind für eine Besprechung bereit.",
"partial": "Teilweiser Test",
"partialHint": "Einige Prüfungen wurden übersprungen. Erlauben Sie den Zugriff auf Ihre Kamera und Ihr Mikrofon, um diese zu testen.",
"failed_one": "{{count}} Prüfung fehlgeschlagen",
"failed_other": "{{count}} Prüfungen fehlgeschlagen",
"failedHint": "Öffnen Sie die fehlgeschlagenen Prüfungen für weitere Details und senden Sie den Bericht an Ihre IT-Abteilung."
},
"help": {
"firewall": "Wenn Netzwerktests fehlschlagen, überprüfen Sie Ihre Browserberechtigungen und die Netzwerkfilterregeln (WebRTC, WebSocket, TURN) mit Ihrer IT-Abteilung."
}
}
-1
View File
@@ -36,7 +36,6 @@
"legalsTerms": "Rechtliche Hinweise",
"data": "Datenschutz und Cookies",
"accessibility": "Barrierefreiheit: nicht konform",
"connectionTest": "Testen Sie Ihre Konfiguration",
"ariaLabel": "neues Fenster",
"codeAnnotation": "Unser Code ist offen und verfügbar in diesem",
"code": "Open-Source-Repository",
-1
View File
@@ -13,7 +13,6 @@
"moreLinkLabel": "Mehr über {{appTitle}} erfahren neuer Tab",
"moreLink": "Mehr erfahren",
"moreAbout": "über {{appTitle}}",
"connectionTestLink": "Testen Sie Ihre Konfiguration",
"createMenu": {
"laterOption": "Meeting für später planen",
"instantOption": "Meeting sofort starten"
+1 -9
View File
@@ -5,14 +5,6 @@
"copyLinkTooltip": "Link kopieren",
"resetLabel": "Zurücksetzen",
"participantLimit": "Bis zu 150 Teilnehmende.",
"popupBlocked": "Popup wurde blockiert. Bitte erlaube Popups für diese Website.",
"settingsTooltip": "Besprechungseinstellungen"
},
"roomSettings": {
"title": "Besprechungseinstellungen",
"description": "Konfiguriere die Besprechung {{roomSlug}}.",
"notAllowed": "Du hast keine Berechtigung, die Einstellungen dieser Besprechung zu ändern.",
"error": "Die Besprechungseinstellungen konnten nicht geladen werden.",
"closeButton": "Schließen"
"popupBlocked": "Popup wurde blockiert. Bitte erlaube Popups für diese Website."
}
}
@@ -1,56 +0,0 @@
{
"title": "Test your configuration",
"runTest": "Run test",
"runAgain": "Run again",
"cancel": "Cancel",
"detailsFor": "Details for {{step}}",
"downloadReport": "Download report",
"homeLink": "Test your configuration",
"progress": "{{done}}/{{total}}",
"progressLabel": "Connection test progress",
"groups": {
"local": "Browser and devices",
"network": "Server connectivity"
},
"steps": {
"browser": "Browser",
"microphone": "Microphone",
"camera": "Camera",
"devices": "Media devices",
"websocket": "WebSocket",
"webrtc": "WebRTC",
"turn": "TURN",
"reconnect": "Reconnect",
"selectedCandidate": "Selected route",
"publishAudio": "Audio publishing",
"publishVideo": "Video publishing"
},
"status": {
"pending": "Pending",
"running": "Running…",
"success": "Passed",
"failed": "Failed",
"skipped": "Skipped"
},
"counts": {
"passed": "passed",
"failed": "failed",
"skipped": "skipped"
},
"summary": {
"idle": "Ready to test",
"idleHint": "The test takes about a minute. Your camera and microphone are only used while it runs.",
"running": "Testing…",
"runningHint": "Keep this page open until every check has finished.",
"passed": "Everything works",
"passedHint": "Your browser, your devices and your network are ready for a meeting.",
"partial": "Partially tested",
"partialHint": "Some checks were skipped. Allow access to your camera and microphone to test them.",
"failed_one": "{{count}} check failed",
"failed_other": "{{count}} checks failed",
"failedHint": "Open the failed checks below for details, then send the report to your IT department."
},
"help": {
"firewall": "If network tests fail, check your browser permissions and network filtering rules (WebRTC, WebSocket, TURN) with your IT department."
}
}
-1
View File
@@ -36,7 +36,6 @@
"legalsTerms": "Legal Notice",
"data": "Personal Data and Cookies",
"accessibility": "Accessibility: non-compliant",
"connectionTest": "Test your configuration",
"ariaLabel": "new window",
"codeAnnotation": "Our code is open and available on this",
"code": "Open Source Code Repository",
-1
View File
@@ -13,7 +13,6 @@
"moreLinkLabel": "Learn more about {{appTitle}} - new tab",
"moreLink": "Learn more",
"moreAbout": "about {{appTitle}}",
"connectionTestLink": "Test your configuration",
"createMenu": {
"laterOption": "Create a meeting for a later date",
"instantOption": "Start an instant meeting"
+1 -9
View File
@@ -5,14 +5,6 @@
"copyLinkTooltip": "Copy link",
"resetLabel": "Reset",
"participantLimit": "Up to 150 participants.",
"popupBlocked": "Popup was blocked. Please allow popups for this site.",
"settingsTooltip": "Meeting settings"
},
"roomSettings": {
"title": "Meeting settings",
"description": "Configure the meeting {{roomSlug}}.",
"notAllowed": "You don't have permission to modify this meeting's settings.",
"error": "Unable to load meeting settings.",
"closeButton": "Close"
"popupBlocked": "Popup was blocked. Please allow popups for this site."
}
}
@@ -1,56 +0,0 @@
{
"title": "Tester votre configuration",
"runTest": "Lancer le test",
"runAgain": "Relancer",
"cancel": "Annuler",
"detailsFor": "Détails de l'étape {{step}}",
"downloadReport": "Télécharger le rapport",
"homeLink": "Tester votre configuration",
"progress": "{{done}}/{{total}}",
"progressLabel": "Progression du test de connexion",
"groups": {
"local": "Navigateur et périphériques",
"network": "Connexion au serveur"
},
"steps": {
"browser": "Navigateur",
"microphone": "Microphone",
"camera": "Caméra",
"devices": "Périphériques médias",
"websocket": "WebSocket",
"webrtc": "WebRTC",
"turn": "TURN",
"reconnect": "Reconnexion",
"selectedCandidate": "Route sélectionnée",
"publishAudio": "Publication audio",
"publishVideo": "Publication vidéo"
},
"status": {
"pending": "En attente",
"running": "En cours…",
"success": "Réussi",
"failed": "Échec",
"skipped": "Ignoré"
},
"counts": {
"passed": "réussis",
"failed": "en échec",
"skipped": "ignorés"
},
"summary": {
"idle": "Prêt à tester",
"idleHint": "Le test dure environ une minute. Votre caméra et votre microphone ne sont utilisés que pendant le test.",
"running": "Test en cours…",
"runningHint": "Gardez cette page ouverte jusqu'à la fin des vérifications.",
"passed": "Tout fonctionne",
"passedHint": "Votre navigateur, vos périphériques et votre réseau sont prêts pour une réunion.",
"partial": "Test partiel",
"partialHint": "Certaines vérifications ont été ignorées. Autorisez l'accès à votre caméra et à votre microphone pour les tester.",
"failed_one": "{{count}} vérification en échec",
"failed_other": "{{count}} vérifications en échec",
"failedHint": "Ouvrez les vérifications en échec pour voir le détail, puis transmettez le rapport à votre service informatique."
},
"help": {
"firewall": "En cas d'échec des tests réseau, vérifiez vos permissions navigateur et les règles de filtrage réseau (WebRTC, WebSocket, TURN) auprès de votre service informatique."
}
}
-1
View File
@@ -36,7 +36,6 @@
"legalsTerms": "Mentions légales",
"data": "Données personnelles et cookie",
"accessibility": "Accessibilité : non conforme",
"connectionTest": "Tester votre configuration",
"ariaLabel": "nouvelle fenêtre",
"codeAnnotation": "Notre code est ouvert et disponible sur ce",
"code": "dépôt de code Open Source",
-1
View File
@@ -13,7 +13,6 @@
"moreLinkLabel": "En savoir plus sur {{appTitle}} - nouvelle fenêtre",
"moreLink": "En savoir plus",
"moreAbout": "sur {{appTitle}}",
"connectionTestLink": "Tester votre configuration",
"createMenu": {
"laterOption": "Créer une réunion pour une date ultérieure",
"instantOption": "Démarrer une réunion instantanée"
+1 -9
View File
@@ -5,14 +5,6 @@
"copyLinkTooltip": "Copier le lien",
"resetLabel": "Réinitialiser",
"participantLimit": "Jusqu'à 150 participants.",
"popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site.",
"settingsTooltip": "Paramètres de la réunion"
},
"roomSettings": {
"title": "Paramètres de la réunion",
"description": "Configurez la réunion {{roomSlug}}.",
"notAllowed": "Vous n'avez pas les droits pour modifier les paramètres de cette réunion.",
"error": "Impossible de charger les paramètres de la réunion.",
"closeButton": "Fermer"
"popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site."
}
}
@@ -1,56 +0,0 @@
{
"title": "Je configuratie testen",
"runTest": "Test starten",
"runAgain": "Opnieuw testen",
"cancel": "Annuleren",
"detailsFor": "Details van stap {{step}}",
"downloadReport": "Rapport downloaden",
"homeLink": "Je configuratie testen",
"progress": "{{done}}/{{total}}",
"progressLabel": "Voortgang van de verbindingstest",
"groups": {
"local": "Browser en apparaten",
"network": "Verbinding met de server"
},
"steps": {
"browser": "Browser",
"microphone": "Microfoon",
"camera": "Camera",
"devices": "Media-apparaten",
"websocket": "WebSocket",
"webrtc": "WebRTC",
"turn": "TURN",
"reconnect": "Opnieuw verbinden",
"selectedCandidate": "Geselecteerde route",
"publishAudio": "Audio publiceren",
"publishVideo": "Video publiceren"
},
"status": {
"pending": "In afwachting",
"running": "Bezig…",
"success": "Geslaagd",
"failed": "Mislukt",
"skipped": "Overgeslagen"
},
"counts": {
"passed": "geslaagd",
"failed": "mislukt",
"skipped": "overgeslagen"
},
"summary": {
"idle": "Klaar om te testen",
"idleHint": "De test duurt ongeveer één minuut. Je camera en microfoon worden alleen tijdens de test gebruikt.",
"running": "Test wordt uitgevoerd…",
"runningHint": "Houd deze pagina open totdat alle controles zijn voltooid.",
"passed": "Alles werkt",
"passedHint": "Je browser, apparaten en netwerk zijn klaar voor een vergadering.",
"partial": "Gedeeltelijke test",
"partialHint": "Sommige controles zijn overgeslagen. Geef toegang tot je camera en microfoon om deze te testen.",
"failed_one": "{{count}} controle mislukt",
"failed_other": "{{count}} controles mislukt",
"failedHint": "Open de mislukte controles voor meer details en stuur het rapport door naar je IT-afdeling."
},
"help": {
"firewall": "Bij mislukte netwerktests: controleer je browserrechten en netwerkfilterregels (WebRTC, WebSocket, TURN) met je IT-afdeling."
}
}
-1
View File
@@ -35,7 +35,6 @@
"legalsTerms": "Wettelijke kennisgeving",
"data": "Persoonlijke gegevens en cookies",
"accessibility": "Toegankelijkheid: audit in uitvoering",
"connectionTest": "Test je configuratie",
"ariaLabel": "nieuw venster",
"codeAnnotation": "Onze code is open en beschikbaar op dit",
"code": "Open Source Code Repository",
-1
View File
@@ -13,7 +13,6 @@
"moreLinkLabel": "Meer informatie over {{appTitle}} - nieuw tabblad",
"moreLink": "Meer informatie",
"moreAbout": "over {{appTitle}}",
"connectionTestLink": "Test je configuratie",
"createMenu": {
"laterOption": "Maak een vergadering voor een latere datum",
"instantOption": "Begin direct een vergadering"
+1 -9
View File
@@ -5,14 +5,6 @@
"copyLinkTooltip": "Link kopiëren",
"resetLabel": "Resetten",
"participantLimit": "Tot 150 deelnemers.",
"popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site.",
"settingsTooltip": "Vergaderinstellingen"
},
"roomSettings": {
"title": "Vergaderinstellingen",
"description": "Configureer de vergadering {{roomSlug}}.",
"notAllowed": "Je hebt geen toestemming om de instellingen van deze vergadering te wijzigen.",
"error": "De vergaderinstellingen konden niet worden geladen.",
"closeButton": "Sluiten"
"popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site."
}
}
+19 -22
View File
@@ -12,7 +12,6 @@ const StyledSwitch = styled(RACSwitch, {
alignItems: 'center',
gap: '0.571rem',
color: 'black',
cursor: 'pointer',
forcedColorAdjust: 'none',
'& .indicator': {
position: 'relative',
@@ -35,29 +34,30 @@ const StyledSwitch = styled(RACSwitch, {
transitionDelay: '0ms',
},
},
'& .checkmark, & .cross': {
'& .checkmark': {
position: 'absolute',
top: 0,
bottom: 0,
width: '1.313rem', // knob width + 2 × knob margin
display: 'grid',
placeItems: 'center',
display: 'block',
top: '50%',
right: '0.1rem',
transform: 'translateY(-50%)',
color: 'primary.800',
fontSize: '0.75rem',
fontWeight: 'bold',
pointerEvents: 'none',
zIndex: 1,
'& svg': {
display: 'block',
width: '0.875rem',
height: '0.875rem',
},
},
'& .checkmark': {
right: 0,
color: 'primary.800',
opacity: 0,
},
'& .cross': {
left: 0,
position: 'absolute',
display: 'block',
top: '50%',
left: '0.13rem',
transform: 'translateY(-50%)',
color: 'white',
fontSize: '0.70rem',
fontWeight: 'bold',
pointerEvents: 'none',
zIndex: 1,
opacity: 1,
transition: 'opacity 200ms',
transitionDelay: '0ms',
@@ -80,9 +80,6 @@ const StyledSwitch = styled(RACSwitch, {
transition: 'opacity 10ms',
transitionDelay: '0ms',
},
'&[data-disabled]': {
cursor: 'not-allowed',
},
'&[data-disabled] .indicator': {
borderColor: 'primary.200',
background: 'transparent',
@@ -114,10 +111,10 @@ export const Switch = ({ children, ...props }: SwitchProps) => (
<>
<div className="indicator">
<span className="checkmark" aria-hidden="true">
<RiCheckLine />
<RiCheckLine size={16} />
</span>
<span className="cross" aria-hidden="true">
<RiCloseFill />
<RiCloseFill size={16} />
</span>
</div>
{typeof children === 'function' ? children(renderProps) : children}
-16
View File
@@ -9,7 +9,6 @@ const CreatePopup = lazy(() => import('@/features/sdk/routes/CreatePopup'))
const CreateMeetingButton = lazy(
() => import('@/features/sdk/routes/CreateMeetingButton')
)
const SettingsPopup = lazy(() => import('@/features/sdk/routes/SettingsPopup'))
const LegalTermsRoute = lazy(
() => import('@/features/legalsTerms/LegalTermsRoute')
)
@@ -21,9 +20,6 @@ const AccessibilityRoute = lazy(
)
const RoomRoute = lazy(() => import('@/features/rooms/routes/Room'))
const FeedbackRoute = lazy(() => import('@/features/rooms/routes/Feedback'))
const ConnectionTestRoute = lazy(
() => import('@/features/diagnostics/routes/ConnectionTest')
)
const roomIdRegex = new RegExp(`^[/](?<roomId>${flexibleRoomIdPattern})$`)
@@ -31,13 +27,11 @@ export const routes: Record<
| 'home'
| 'room'
| 'feedback'
| 'connectionTest'
| 'legalTerms'
| 'accessibility'
| 'termsOfService'
| 'sdkCreatePopup'
| 'sdkCreateButton'
| 'sdkSettingsPopup'
| 'recordingDownload',
{
name: RouteName
@@ -63,11 +57,6 @@ export const routes: Record<
path: '/feedback',
Component: FeedbackRoute,
},
connectionTest: {
name: 'connectionTest',
path: '/test-connection',
Component: ConnectionTestRoute,
},
legalTerms: {
name: 'legalTerms',
path: '/mentions-legales',
@@ -93,11 +82,6 @@ export const routes: Record<
path: '/sdk/create-button',
Component: CreateMeetingButton,
},
sdkSettingsPopup: {
name: 'sdkSettingsPopup',
path: '/sdk/settings-popup',
Component: SettingsPopup,
},
recordingDownload: {
name: 'recordingDownload',
path: /^\/recording\/(?<recordingId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/,
-13
View File
@@ -31,19 +31,6 @@ html.font-opendyslexic {
border: 0;
}
/* LiveKit ConnectionCheck appends a temporary <video> to body during publishVideo.
Keep it decodable (not display:none) but invisible. */
body.connection-test-hide-livekit-video > video {
position: fixed;
top: 0;
left: 0;
width: 10px;
height: 10px;
opacity: 0;
pointer-events: none;
z-index: -1;
}
* {
outline: 2px solid transparent;
}
-14
View File
@@ -26,20 +26,6 @@ export default defineConfig(({ mode }) => {
dest: 'assets/mediapipe/wasm',
rename: { stripBase: 4 },
},
{
// Kept out of public/ so the instance title reaches the manifest,
// the way index.html gets it through %VITE_APP_TITLE%.
src: 'site.webmanifest',
dest: '.',
transform: (content) => {
const title = env.VITE_APP_TITLE
return JSON.stringify({
...JSON.parse(content),
name: title,
short_name: title,
})
},
},
],
}),
env.VITE_ANALYZE === 'true' &&
+5 -5
View File
@@ -10,16 +10,16 @@ dependencies = [
"celery==5.6.3",
"redis==5.2.1",
"minio==7.2.20",
"openai==2.48.0",
"posthog==7.29.0",
"openai==2.44.0",
"posthog==7.20.5",
"requests==2.34.2",
"sentry-sdk[fastapi, celery]==2.66.1",
"langfuse==4.14.1"
"sentry-sdk[fastapi, celery]==2.63.0",
"langfuse==4.11.0"
]
[project.optional-dependencies]
dev = [
"ruff==0.16.0",
"ruff==0.15.19",
"pytest==9.1.1",
"responses>=0.25.8",
]
+2 -2
View File
@@ -578,7 +578,7 @@ def task_retry_handler_transcript(request=None, reason=None, einfo=None, **kwarg
@signals.task_failure.connect(sender=process_audio_transcribe_v2_task)
def handle_transcribe_v2_failed( # noqa: PLR0917
def handle_transcribe_v2_failed(
sender,
task_id=None,
exception=None,
@@ -674,7 +674,7 @@ def task_retry_handler_summary(request=None, reason=None, einfo=None, **kwargs):
@signals.task_failure.connect(sender=summarize_v2_task)
def handle_summarize_v2_failed( # noqa: PLR0917
def handle_summarize_v2_failed(
sender,
task_id=None,
exception=None,